WIP initial commit with working greeting and login calls
This commit is contained in:
commit
f503132d85
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
/misc
|
||||
Cargo.lock
|
|
@ -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 = "*"
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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(®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)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod objects;
|
||||
pub mod request;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
pub trait EppXml {
|
||||
fn serialize(&self) -> Result<String, Box<dyn Error>>;
|
||||
fn deserialize(&self) -> Result<Self, Box<dyn Error>>;
|
||||
}
|
|
@ -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 {
|
||||
|
||||
// }
|
||||
// }
|
|
@ -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());
|
||||
// }
|
||||
// }
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue