WIP initial commit with working greeting and login calls

This commit is contained in:
Ritesh Chitlangi 2021-07-17 03:16:28 +08:00
commit f503132d85
11 changed files with 538 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
/misc
Cargo.lock

20
Cargo.toml Normal file
View File

@ -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 = "*"

47
src/config.rs Normal file
View File

@ -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<String, EppClientConnection>,
}
impl default::Default for EppClientConfig {
fn default() -> Self {
let servers: HashMap<String, EppClientConnection> = 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)
}
}

185
src/connection.rs Normal file
View File

@ -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<TcpStream>,
pub greeting: String,
}
pub struct EppClient {
connection: EppConnection,
}
impl EppConnection {
pub async fn new(
registry: String,
credentials: (String, String),
mut stream: TlsStream<TcpStream>) -> Result<EppConnection, Box<dyn Error>> {
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<u8>) -> Result<(), Box<dyn Error>> {
self.stream.write_all(buf).await?;
Ok(())
}
pub async fn send_epp_request(&mut self, content: &str) -> Result<(), Box<dyn Error>> {
let len = content.len();
let buf_size = len + 4;
let mut buf: Vec<u8> = 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<Vec<u8>, Box<dyn Error>> {
let mut buf = vec![0u8; 4096];
self.stream.read(&mut buf).await?;
Ok(buf)
}
async fn read_epp_response(&mut self) -> Result<Vec<u8>, Box<dyn Error>> {
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<String, Box<dyn Error>> {
let contents = self.read().await?;
let response = str::from_utf8(&contents)?.to_string();
Ok(response)
}
pub async fn transact(&mut self, content: &str) -> Result<String, Box<dyn Error>> {
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<dyn Error>> {
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<String, Box<dyn Error>> {
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<String, Box<dyn Error>> {
self.connection.transact(&xml).await
}
pub fn greeting(&self) -> String {
return String::from(&self.connection.greeting)
}
}
async fn epp_connect(registry_creds: &EppClientConnection) -> Result<TlsStream<TcpStream>, 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<EppClient, Box<dyn Error>> {
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(&registry_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)
}

2
src/epp.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod objects;
pub mod request;

0
src/epp/objects.rs Normal file
View File

169
src/epp/request.rs Normal file
View File

@ -0,0 +1,169 @@
use quick_xml::se;
use serde::{Deserialize, Serialize, Serializer};
use std::error::Error;
const EPP_XML_HEADER: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>"#;
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<String, Box<dyn Error>> {
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<Vec<StringValue>>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct Services {
#[serde(rename = "objURI")]
obj_uris: Vec<StringValue>,
#[serde(rename = "svcExtension")]
svc_ext: Option<ServiceExtension>,
}
#[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;
}
}

4
src/epp/xml.rs Normal file
View File

@ -0,0 +1,4 @@
pub trait EppXml {
fn serialize(&self) -> Result<String, Box<dyn Error>>;
fn deserialize(&self) -> Result<Self, Box<dyn Error>>;
}

39
src/error.rs Normal file
View File

@ -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<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Self::EppConnectionError(e)
}
}
impl From<std::io::ErrorKind> for Error {
fn from(e: std::io::ErrorKind) -> Self {
Self::EppConnectionError(std::io::Error::from(e))
}
}
impl From<String> for Error {
fn from(e: String) -> Self {
Self::Other(e)
}
}
// impl From<std::io::Error> for Box<EppClientError> {
// fn from(e: std::io::Error) -> Self {
// }
// }

35
src/lib.rs Normal file
View File

@ -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());
// }
// }

34
src/main.rs Normal file
View File

@ -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);
}