You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
7.3 KiB

//! This crate handles all the common types used from request and response, as well as how they
//! should be linked together.
//! For example, an `impl LunaNodeRequest` has an associated type `Response` that must be set to a
//! valid LunaNodeResponse on creation.
//!
//! Although this can all be done automatically through macros, it is important that the
//! relationships be defined somewhere.
//! This is the location for those relationships.
//!
//! If you'd like to see how to implement the macros, check the the `lunanode_macros` subcrate.
use crate::success;
use clap;
use parse_display_derive::Display;
use hmac::{Hmac, Mac};
use serde::{
Serialize,
Deserialize,
de::DeserializeOwned,
};
use sha2::Sha512;
use ureq::post;
type HmacSHA512 = Hmac<Sha512>;
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, clap::Args, Clone, Display)]
#[display("{api_id}")]
/// Used to specify API credentials. These are not required as long as LUNANODE_KEY_ID and LUNANODE_API_KEY are specified in the environment.
pub struct ApiKeys {
#[clap(long, env="LUNANODE_KEY_ID", help="The API key ID from the Lunanode Dashboard.", long_help="The API key ID from the Lunanode dashboard. You may also specify this option via the LUNANODE_KEY_ID environment variable.", required=false)]
/// The API id string as used by LunaNode. This should be exactly 16 bytes. This will get enforced at a later date.
pub api_id: String,
#[serde(skip)]
#[clap(long, env="LUNANODE_API_KEY", help="The Lunanode API key received from the dashboard.", long_help="The Lunanode API key recieved from the dashboard. This option may also be set by the LUNANODE_API_KEY environemnt variable.", required=false)]
/// Used for convenience. It is not serialized nor deserializes; this means that this object can NOT be deserialized at all, since this is not an Option<String>.
/// This is used to fill in the api_paritlakey field, and to give the request a way to access the api key locally.
pub api_key: String,
#[clap(long, env="LUNANODE_API_PARTIALKEY", help="The Lunanode API key received from the dashboard, but only the first 64 bytes of it.", long_help="The Lunanode API key recieved from the dashboard. This option may also be set by the LUNANODE_API_PARTIALKEY environemnt variable. This should be autogenerated based on the LUNANODE_API_KEY, but it isn't. TODO.", required=false)]
/// The api_paritalkey field as used by LunaNode (the first 64 bytes of the api_key). Note that this field is autofilled.
/// Since this should always be a 64 byte array, this will be enforced at some point in the future. TODO
pub api_partialkey: String,
}
/// A trait that must be implemented by any response type.
pub trait LunaNodeResponse: DeserializeOwned {}
/// A trait that must be implemented by any request type.
/// This can be done with:
/// ```rust
/// // struct ResponseType {}
/// #[lunanode_request(response="ResponseType", endpoint="vm/list/")]
/// pub struct VmListRequest {}
/// ```
pub trait LunaNodeRequest: Serialize + std::fmt::Debug {
/// The resposne type you expect after making this request.
/// Setting this will allow acces to a generic function, make_request, that will expect this type to be recieved.
/// See: make_request
type Response: LunaNodeResponse;
/// Can't genericize this one out. You need to implenent a function which can return a set of API keys for creating an API requst.
fn get_keys(&self) -> ApiKeys;
/// The LunaNode API endpoint you'll be using.
/// Note: do *NOT* add a leading slash, but *DO* include a trailing slash.
/// The three wrong ways: "/vm/list", "vm/list", "/vm/list",
/// The only correct way: "vm/list/".
fn url_endpoint(&self) -> String;
/// A generic function to recieve a specific relsponse type from the server.
/// The Self::response type is the type you're expecting. In the case that this type is incorrect, or the server returns bad data, you will receive an LunaNodeError::SerdeError(serde::Error, String), with the String being a raw response from the server.
/// You may also recieve any other error defined in LunaNodeError, including timezone errors due to not being able to create a nonce value for the request.
/// See LunaNodeError.
fn make_request(&self) -> Result<Self::Response, LunaNodeError> {
make_request::<Self, Self::Response>(self)
}
}
#[derive(Serialize, Deserialize, Debug)]
/// This response is given if the request was successful (technically), but some error has occured
/// to make the request not possible from Lunanode's standpoint.
pub struct LunaNodeErrorResponse {
#[serde(with="success")]
success: bool,
error: String, // proper type, full accoutnign of the error from the API
}
impl std::fmt::Display for LunaNodeErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "An API error has occured! {}", self.error)
}
}
impl std::error::Error for LunaNodeErrorResponse {}
#[derive(Debug)]
/// A common error type for the entire crate.
pub enum LunaNodeError {
/// Pass through a request error from ureq.
RequestError(ureq::Error),
/// Pass through a deserializeation error; this is just an std::io::Error that occurs when it is
/// not possible to read the string where deserialization would occur.
DeserializationError(std::io::Error),
/// Pass through an error when finding the system time.
TimeError(std::time::SystemTimeError),
/// An error from the Lunanode API
LunaNodeError(LunaNodeErrorResponse),
/// Pass through a serde error to the user.
SerdeError(serde_json::Error, String), // the serde error, accompanied by a raw string
}
impl std::fmt::Display for LunaNodeError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "An error has occured! {}", self)
}
}
impl std::error::Error for LunaNodeError {}
fn make_request<S, D>(req: &S) -> Result<D, LunaNodeError>
where S: LunaNodeRequest + ?Sized,
D: LunaNodeResponse
{
let json_req_data = match serde_json::to_string(req) {
Ok(jrd) => jrd,
Err(e) => return Err(LunaNodeError::SerdeError(e, format!("{:?}", req))),
};
let epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Err(e) => return Err(LunaNodeError::TimeError(e)),
Ok(ue) => ue,
}.as_secs();
let nonce = format!("{}", epoch);
let mut hmac = HmacSHA512::new_from_slice(req.get_keys().api_key.as_bytes()).expect("SHA512 accepts text of any size.");
let hmac_input = format!("{}|{}|{}", req.url_endpoint(), &json_req_data, nonce);
hmac.update(hmac_input.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
let full_url = format!("https://dynamic.lunanode.com/api/{}", req.url_endpoint());
match post(&full_url)
.send_form(&[
("req", &json_req_data),
("signature", &signature),
("nonce", &nonce),
]) {
Err(e) => Err(LunaNodeError::RequestError(e)),
Ok(resp) => {
let resp_str = match resp.into_string() {
Err(e) => return Err(LunaNodeError::DeserializationError(e)),
Ok(s) => s,
};
println!("RESP: {}", resp_str);
match serde_json::from_str::<LunaNodeErrorResponse>(&resp_str) {
Ok(e) => return Err(LunaNodeError::LunaNodeError(e)),
Err(_e) => false,
};
match serde_json::from_str::<D>(&resp_str) {
Ok(s) => Ok(s),
Err(e) => Err(LunaNodeError::SerdeError(e, resp_str)),
}
},
}
}