commit f503132d85430f8cb110fa32d37e5f146214e066 Author: Ritesh Chitlangi Date: Sat Jul 17 03:16:28 2021 +0800 WIP initial commit with working greeting and login calls diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a08a4e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/misc +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..201a4e4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "epp-client" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +confy = "0.4" +futures = "0.3" +lazy_static = "1.4" +quick-xml = { version = "0.22", features = [ "serialize" ] } +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.0", features = [ "full" ] } +tokio-rustls = "0.22" +webpki = "0.22" +webpki-roots = "0.21" + +[dev-dependencies] +tokio-test = "*" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..2edccd7 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,47 @@ +use confy; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::default; + +lazy_static! { + pub static ref CONFIG: EppClientConfig = match confy::load("epp-client") { + Ok(cfg) => cfg, + Err(e) => panic!("Config read error: {}", e), + }; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct EppClientConnection { + host: String, + port: u16, + username: String, + password: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct EppClientConfig { + pub servers: HashMap, +} + +impl default::Default for EppClientConfig { + fn default() -> Self { + let servers: HashMap = HashMap::new(); + Self { servers: servers } + } +} + +impl EppClientConnection { + pub fn connection_details(&self) -> (String, u16) { + (self.host.to_string(), self.port) + } + pub fn credentials(&self) -> (String, String) { + (self.username.to_string(), self.password.to_string()) + } +} + +impl EppClientConfig { + pub fn registry(&self, registry: &str) -> Option<&EppClientConnection> { + self.servers.get(registry) + } +} diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..b3006b6 --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,185 @@ +use std::sync::Arc; +use std::sync::mpsc; +use std::{str, u32}; +use std::convert::TryInto; +use futures::executor::block_on; +use std::{error::Error, net::ToSocketAddrs, io as stdio}; +use tokio_rustls::{TlsConnector, rustls::ClientConfig, webpki::DNSNameRef, client::TlsStream}; +use tokio::{net::TcpStream, io::AsyncWriteExt, io::AsyncReadExt}; + +use crate::config::{CONFIG, EppClientConnection}; +use crate::error; +use crate::epp::request::EppRequest; + +struct EppConnection { + registry: String, + credentials: (String, String), + stream: TlsStream, + pub greeting: String, +} + +pub struct EppClient { + connection: EppConnection, +} + +impl EppConnection { + pub async fn new( + registry: String, + credentials: (String, String), + mut stream: TlsStream) -> Result> { + let mut buf = vec![0u8; 4096]; + stream.read(&mut buf).await?; + let greeting = str::from_utf8(&buf)?.to_string(); + + Ok(EppConnection { + registry: registry, + credentials: credentials, + stream: stream, + greeting: greeting + }) + } + + async fn write(&mut self, buf: &Vec) -> Result<(), Box> { + self.stream.write_all(buf).await?; + Ok(()) + } + + pub async fn send_epp_request(&mut self, content: &str) -> Result<(), Box> { + let len = content.len(); + + let buf_size = len + 4; + let mut buf: Vec = vec![0u8; buf_size]; + + let len_u32: [u8; 4] = u32::to_be_bytes(len.try_into()?); + + buf[..4].clone_from_slice(&len_u32); + buf[4..].clone_from_slice(&content.as_bytes()); + + self.write(&buf).await + } + + async fn read(&mut self) -> Result, Box> { + let mut buf = vec![0u8; 4096]; + self.stream.read(&mut buf).await?; + Ok(buf) + } + + async fn read_epp_response(&mut self) -> Result, Box> { + let mut buf = [0u8; 4]; + self.stream.read_exact(&mut buf).await?; + + let buf_size :usize = u32::from_be_bytes(buf).try_into()?; + + println!("Response buffer size: {}", buf_size); + + let mut buf = vec![0u8; buf_size - 4]; + + self.stream.read(&mut buf).await?; + + Ok(buf) + } + + pub async fn get_epp_response(&mut self) -> Result> { + let contents = self.read().await?; + + let response = str::from_utf8(&contents)?.to_string(); + + Ok(response) + } + + pub async fn transact(&mut self, content: &str) -> Result> { + let content = format!("{}\r\n\r\n", content); + + self.send_epp_request(&content).await?; + self.get_epp_response().await + } + + async fn close(&mut self) -> Result<(), Box> { + println!("Closing ..."); + + self.stream.shutdown().await?; + Ok(()) + } +} + +impl Drop for EppConnection { + fn drop(&mut self) { + block_on(self.close()); + } +} + +impl EppClient { + pub async fn transact(&mut self, request: &EppRequest) -> Result> { + let epp_xml = request.to_epp_xml()?; + + println!("Request:\r\n{}", epp_xml); + + let response = self.connection.transact(&epp_xml).await?; + println!("Response:\r\n{}", response); + + Ok(response) + } + + pub async fn transact_xml(&mut self, xml: &str) -> Result> { + self.connection.transact(&xml).await + } + + pub fn greeting(&self) -> String { + return String::from(&self.connection.greeting) + } +} + +async fn epp_connect(registry_creds: &EppClientConnection) -> Result, error::Error> { + let (host, port) = registry_creds.connection_details(); + + println!("{}: {}", host, port); + + let addr = (host.as_str(), port) + .to_socket_addrs()? + .next() + .ok_or_else(|| stdio::ErrorKind::NotFound)?; + + let mut config = ClientConfig::new(); + + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + + let connector = TlsConnector::from(Arc::new(config)); + let stream = TcpStream::connect(&addr).await?; + + let domain = DNSNameRef::try_from_ascii_str(&host) + .map_err(|_| stdio::Error::new(stdio::ErrorKind::InvalidInput, format!("Invalid domain: {}", host)))?; + + let stream = connector.connect(domain, stream).await?; + + Ok(stream) +} + +pub async fn connect(registry: &'static str) -> Result> { + let registry_creds = match CONFIG.registry(registry) { + Some(creds) => creds, + None => return Err(format!("missing credentials for {}", registry).into()) + }; + + let (tx, rx) = mpsc::channel(); + + tokio::spawn(async move { + let stream = epp_connect(®istry_creds).await.unwrap(); + let credentials = registry_creds.credentials(); + + let connection = EppConnection::new( + registry.to_string(), + credentials, + stream + ).await.unwrap(); + + let client = EppClient { connection: connection }; + + tx.send(client).unwrap(); + }); + + let client = rx.recv()?; + + Ok(client) +} diff --git a/src/epp.rs b/src/epp.rs new file mode 100644 index 0000000..a21b298 --- /dev/null +++ b/src/epp.rs @@ -0,0 +1,2 @@ +pub mod objects; +pub mod request; diff --git a/src/epp/objects.rs b/src/epp/objects.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/epp/request.rs b/src/epp/request.rs new file mode 100644 index 0000000..3e4dbcc --- /dev/null +++ b/src/epp/request.rs @@ -0,0 +1,169 @@ +use quick_xml::se; +use serde::{Deserialize, Serialize, Serializer}; +use std::error::Error; + +const EPP_XML_HEADER: &str = r#""#; +const EPP_XMLNS: &str = "urn:ietf:params:xml:ns:epp-1.0"; +const EPP_XMLNS_XSI: &str = "http://www.w3.org/2001/XMLSchema-instance"; +const EPP_XSI_SCHEMA_LOCATION: &str = "urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd"; + +const EPP_VERSION: &str = "1.0"; +const EPP_LANG: &str = "en"; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct StringValue(String); + +impl Default for StringValue { + fn default() -> Self { + Self(String::from("")) + } +} + +pub trait StringValueTrait { + fn to_string_value(&self) -> StringValue; +} + +impl StringValueTrait for &str { + fn to_string_value(&self) -> StringValue { + StringValue(self.to_string()) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum RequestType { + Hello, + Command { + login: Login, + #[serde(rename = "clTRIDv")] + client_tr_id: StringValue, + }, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename = "epp")] +pub struct EppObject { + xmlns: String, + #[serde(rename = "xmlns:xsi")] + xmlns_xsi: String, + #[serde(rename = "xsi:schemaLocation")] + xsi_schema_location: String, + data: RequestType, +} + +impl EppObject { + pub fn new(data: RequestType) -> EppObject { + EppObject { + data: data, + xmlns: EPP_XMLNS.to_string(), + xmlns_xsi: EPP_XMLNS_XSI.to_string(), + xsi_schema_location: EPP_XSI_SCHEMA_LOCATION.to_string(), + } + } + + pub fn to_epp_xml(&self) -> Result> { + let epp_xml = format!("{}\r\n{}", EPP_XML_HEADER, se::to_string(self)?); + + Ok(epp_xml) + } +} + +pub type EppRequest = EppObject; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +pub struct Hello; + +impl Hello { + pub fn new() -> EppRequest { + EppRequest::new(RequestType::Hello) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename = "options")] +pub struct LoginOptions { + version: StringValue, + lang: StringValue, +} + +impl LoginOptions { + pub fn build(version: &str, lang: &str) -> LoginOptions { + LoginOptions { + version: version.to_string_value(), + lang: lang.to_string_value(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename = "svcExtension")] +pub struct ServiceExtension { + #[serde(rename = "extURI")] + ext_uris: Option>, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct Services { + #[serde(rename = "objURI")] + obj_uris: Vec, + #[serde(rename = "svcExtension")] + svc_ext: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct Command { + login: Login, + #[serde(rename = "clTRID")] + client_tr_id: StringValue, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename = "login")] +pub struct Login { + #[serde(rename(serialize = "clID", deserialize = "clID"))] + username: StringValue, + #[serde(rename = "pw", default)] + password: StringValue, + options: LoginOptions, + #[serde(rename = "svcs")] + services: Services, +} + +impl Login { + pub fn new(username: &str, password: &str, client_tr_id: &str) -> EppRequest { + let login = Login { + username: username.to_string_value(), + password: password.to_string_value(), + options: LoginOptions { + version: EPP_VERSION.to_string_value(), + lang: EPP_LANG.to_string_value(), + }, + services: Services { + obj_uris: vec![ + "urn:ietf:params:xml:ns:host-1.0".to_string_value(), + "urn:ietf:params:xml:ns:contact-1.0".to_string_value(), + "urn:ietf:params:xml:ns:domain-1.0".to_string_value(), + ], + svc_ext: Some(ServiceExtension { + ext_uris: Some(vec![ + "http://schema.ispapi.net/epp/xml/keyvalue-1.0".to_string_value() + ]), + }), + }, + }; + + EppRequest::new(RequestType::Command { + login: login, + client_tr_id: client_tr_id.to_string_value(), + }) + } + + pub fn set_options(&mut self, options: LoginOptions) { + self.options = options; + } + + pub fn set_services(&mut self, services: Services) { + self.services = services; + } +} diff --git a/src/epp/xml.rs b/src/epp/xml.rs new file mode 100644 index 0000000..3588cbe --- /dev/null +++ b/src/epp/xml.rs @@ -0,0 +1,4 @@ +pub trait EppXml { + fn serialize(&self) -> Result>; + fn deserialize(&self) -> Result>; +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..50d0557 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,39 @@ +use std::fmt::Display; + +#[derive(Debug)] +pub enum Error { + EppConnectionError(std::io::Error), + Other(String), +} + +impl std::error::Error for Error {} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "epp-client Exception: {:?}", self) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Self::EppConnectionError(e) + } +} + +impl From for Error { + fn from(e: std::io::ErrorKind) -> Self { + Self::EppConnectionError(std::io::Error::from(e)) + } +} + +impl From for Error { + fn from(e: String) -> Self { + Self::Other(e) + } +} + +// impl From for Box { +// fn from(e: std::io::Error) -> Self { + +// } +// } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1dc6ae0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,35 @@ +// pub mod config; +// pub mod connection; + +// #[cfg(test)] +// mod tests { +// use super::config; +// use super::connection; +// use std::str; + +// #[test] +// fn config() { +// let servers = &config::CONFIG.servers; + +// () +// } + +// macro_rules! aw { +// ($e:expr) => { +// tokio_test::block_on($e) +// }; +// } + +// #[test] +// fn connect() { +// let mut cn = aw!(connection::connect("hexonet")).unwrap(); +// println!("lol"); +// let contents = aw!(cn.read()).unwrap(); + +// match str::from_utf8(&contents) { +// Ok(v) => println!("{}", v), +// Err(e) => panic!("Error: {}", e) +// } +// aw!(cn.close()); +// } +// } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1133fa1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,34 @@ +pub mod config; +pub mod connection; +pub mod epp; +pub mod error; + +use std::time::SystemTime; +use crate::{epp::request}; + +#[tokio::main] +async fn main() { + let mut client = match connection::connect("hexonet").await { + Ok(client) => { + println!("{}", client.greeting()); + client + }, + Err(e) => panic!("Error: {}", e) + }; + + let epp_hello = request::Hello::new(); + + client.transact(&epp_hello).await.unwrap(); + + let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); + let cl_trid = format!("eppdev:{}", timestamp.as_secs()); + let epp_login = request::Login::new("eppdev", "sh48sja#27*A", &cl_trid); + + // let response = epp_login.to_epp_xml().unwrap(); + + client.transact(&epp_login).await.unwrap(); + + //let response = client.transact(&epp_hello).await.unwrap(); + + //println!("{}", response); +}