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
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)),
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|