use futures_util::Stream;
use log::debug;
use rustls::{ClientConfig, RootCertStore};
use serde_json::Value;
use std::{fs, path::Path, time::Duration};
use reqwest::Client;
use crate::{
CertsResponse, DAnalyticsResponse, DAuditResponse, DiscardResponse, ListDomainResponse,
ListResponse, ListResult, ManifestResponse, MetadataResponse, PlansResponse, RollResponse,
TeardownResponse,
config::Config,
error::{ApiErrorResponse, SurgeError},
responses::{AccountResponse, LoginResponse},
types::{Auth, Event},
};
pub struct SurgeSdk {
pub config: Config,
pub client: Client,
}
impl SurgeSdk {
pub fn new(config: Config) -> Result<Self, SurgeError> {
let client = if cfg!(feature = "rustls") {
rustls::crypto::ring::default_provider()
.install_default()
.map_err(|e| SurgeError::Http(format!("Failed to set crypto provider: {:?}", e)))?;
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let tls_confg = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.danger_accept_invalid_certs(config.insecure)
.use_preconfigured_tls(tls_confg)
.build()
.map_err(|e| SurgeError::Http(e.to_string()))?
} else {
Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.danger_accept_invalid_certs(config.insecure)
.build()
.map_err(|e| SurgeError::Http(e.to_string()))?
};
Ok(Self { config, client })
}
pub async fn account(&self, auth: &Auth) -> Result<AccountResponse, SurgeError> {
let url = self.config.endpoint.join("account")?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sended to account: {:#?}", req);
let res = req.send().await?.json().await?;
debug!("Response received: {:#?}", res);
Ok(res)
}
pub async fn list(&self, domain: Option<&str>, auth: &Auth) -> Result<ListResult, SurgeError> {
let path = match domain {
Some(d) => format!("{}/list", d),
None => "list".to_string(),
};
let url = self.config.endpoint.join(&path)?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to list: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
match domain {
Some(_) => {
let domain_response: ListDomainResponse = serde_json::from_str(&body_text)?;
Ok(ListResult::Domain(domain_response))
}
None => {
let global_response: Vec<ListResponse> = serde_json::from_str(&body_text)?;
Ok(ListResult::Global(global_response))
}
}
}
pub async fn nuke(&self, auth: &Auth) -> Result<(), SurgeError> {
let url = self.config.endpoint.join("account")?;
let req = self.apply_auth(self.client.delete(url), auth);
debug!("Request sent to nuke: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn teardown(
&self,
domain: &str,
auth: &Auth,
) -> Result<TeardownResponse, SurgeError> {
let url = self.config.endpoint.join(domain)?;
let req = self.apply_auth(self.client.delete(url), auth);
debug!("Request sent to teardown: {:#?}", &req);
let response = req.send().await?;
let body_text = response.text().await?;
debug!("response raw: {:?}", body_text);
let teardown_response: TeardownResponse = serde_json::from_str(&body_text)?;
Ok(teardown_response)
}
pub async fn login(&self, auth: &Auth) -> Result<LoginResponse, SurgeError> {
let url = self.config.endpoint.join("token")?;
let req = self.apply_auth(self.client.post(url), auth);
debug!("Request sent to login: {:#?}", req);
let res = req.send().await?;
let status = res.status();
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
if status.is_success() {
let login_response: LoginResponse = serde_json::from_str(&body_text)?;
Ok(login_response)
} else {
match serde_json::from_str::<ApiErrorResponse>(&body_text) {
Ok(api_error) => Err(SurgeError::Api {
status: api_error.status,
message: api_error.errors.join("; "),
details: api_error.details,
}),
Err(_) => Err(SurgeError::Http(format!(
"HTTP error: status {}, body: {}",
status, body_text
))),
}
}
}
pub async fn publish(
&self,
project_path: &Path,
domain: &str,
auth: &Auth,
headers: Option<Vec<(String, String)>>,
argv: Option<&[String]>,
) -> Result<impl Stream<Item = Result<Event, SurgeError>>, SurgeError> {
crate::stream::publish(self, project_path, domain, auth, headers, argv).await
}
pub async fn publish_wip(
&self,
project_path: &Path,
domain: &str,
auth: &Auth,
headers: Option<Vec<(String, String)>>,
argv: Option<&[String]>,
) -> Result<impl Stream<Item = Result<Event, SurgeError>>, SurgeError> {
crate::stream::publish_wip(self, project_path, domain, auth, headers, argv).await
}
pub async fn rollback(&self, domain: &str, auth: &Auth) -> Result<RollResponse, SurgeError> {
let url = self.config.endpoint.join(&format!("{}/rollback", domain))?;
let req = self.apply_auth(self.client.post(url), auth);
debug!("Request sent to rollback: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let rollback_response: RollResponse = serde_json::from_str(&body_text)?;
Ok(rollback_response)
}
pub async fn rollfore(&self, domain: &str, auth: &Auth) -> Result<RollResponse, SurgeError> {
let url = self.config.endpoint.join(&format!("{}/rollfore", domain))?;
let req = self.apply_auth(self.client.post(url), auth);
debug!("Request sent to rollfore: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let rollfore_response: RollResponse = serde_json::from_str(&body_text)?;
Ok(rollfore_response)
}
pub async fn cutover(
&self,
domain: &str,
revision: Option<&str>,
auth: &Auth,
) -> Result<(), SurgeError> {
let path = match revision {
Some(rev) => format!("{}/rev/{}", domain, rev),
None => format!("{}/rev", domain),
};
let url = self.config.endpoint.join(&path)?;
let req = self.apply_auth(self.client.put(url), auth);
debug!("Request sent to cutover: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn discard(
&self,
revision: &str,
auth: &Auth,
) -> Result<DiscardResponse, SurgeError> {
let url = self.config.endpoint.join(&format!("{}/rev", revision))?;
let req = self.apply_auth(self.client.delete(url), auth);
debug!("Request sent to discard: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let discard_response: DiscardResponse = serde_json::from_str(&body_text)?;
Ok(discard_response)
}
pub async fn certs(&self, domain: &str, auth: &Auth) -> Result<CertsResponse, SurgeError> {
let url = self.config.endpoint.join(&format!("{}/certs", domain))?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to certs: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let certs_response: CertsResponse = serde_json::from_str(&body_text)?;
Ok(certs_response)
}
pub async fn metadata(
&self,
domain: &str,
revision: Option<&str>,
auth: &Auth,
) -> Result<MetadataResponse, SurgeError> {
let path = match revision {
Some(rev) => format!("{}/{}/metadata.json", domain, rev),
None => format!("{}/metadata.json", domain),
};
let url = self.config.endpoint.join(&path)?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to metadata: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let metadata_response: MetadataResponse = serde_json::from_str(&body_text)?;
Ok(metadata_response)
}
pub async fn manifest(
&self,
domain: &str,
revision: Option<&str>,
auth: &Auth,
) -> Result<ManifestResponse, SurgeError> {
let path = match revision {
Some(rev) => format!("{}/{}/manifest.json", domain, rev),
None => format!("{}/manifest.json", domain),
};
let url = self.config.endpoint.join(&path)?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to manifest: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let manifest_response: ManifestResponse = serde_json::from_str(&body_text)?;
Ok(manifest_response)
}
pub async fn files(&self, domain: &str, auth: &Auth) -> Result<ManifestResponse, SurgeError> {
self.manifest(domain, None, auth).await
}
pub async fn config(
&self,
domain: &str,
settings: Value,
auth: &Auth,
) -> Result<(), SurgeError> {
let url = self.config.endpoint.join(&format!("{}/settings", domain))?;
let req = self.apply_auth(self.client.put(url), auth).json(&settings);
debug!("Request sent to config: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn dns(&self, domain: &str, auth: &Auth) -> Result<Value, SurgeError> {
let url = self.config.endpoint.join(&format!("{}/dns", domain))?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to dns: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let dns_response: Value = serde_json::from_str(&body_text)?;
Ok(dns_response)
}
pub async fn dns_add(
&self,
domain: &str,
record: Value,
auth: &Auth,
) -> Result<(), SurgeError> {
let url = self.config.endpoint.join(&format!("{}/dns", domain))?;
let req = self.apply_auth(self.client.post(url), auth).json(&record);
debug!("Request sent to dns_add: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn dns_remove(&self, domain: &str, id: &str, auth: &Auth) -> Result<(), SurgeError> {
let url = self
.config
.endpoint
.join(&format!("{}/dns/{}", domain, id))?;
let req = self.apply_auth(self.client.delete(url), auth);
debug!("Request sent to dns_remove: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn zone(&self, domain: &str, auth: &Auth) -> Result<Value, SurgeError> {
let url = self.config.endpoint.join(&format!("{}/zone", domain))?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to zone: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let zone_response: Value = serde_json::from_str(&body_text)?;
Ok(zone_response)
}
pub async fn zone_add(
&self,
domain: &str,
record: Value,
auth: &Auth,
) -> Result<(), SurgeError> {
let url = self.config.endpoint.join(&format!("{}/zone", domain))?;
let req = self.apply_auth(self.client.post(url), auth).json(&record);
debug!("Request sent to zone_add: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn zone_remove(&self, domain: &str, id: &str, auth: &Auth) -> Result<(), SurgeError> {
let url = self
.config
.endpoint
.join(&format!("{}/zone/{}", domain, id))?;
let req = self.apply_auth(self.client.delete(url), auth);
debug!("Request sent to zone_remove: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn bust(&self, domain: &str, auth: &Auth) -> Result<(), SurgeError> {
let url = self.config.endpoint.join(&format!("{}/cache", domain))?;
let req = self.apply_auth(self.client.delete(url), auth);
debug!("Request sent to bust: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn stats(&self, auth: &Auth) -> Result<Value, SurgeError> {
let url = self.config.endpoint.join("stats")?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to stats: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let stats_response: Value = serde_json::from_str(&body_text)?;
Ok(stats_response)
}
pub async fn analytics(
&self,
domain: &str,
auth: &Auth,
) -> Result<DAnalyticsResponse, SurgeError> {
let url = self
.config
.endpoint
.join(&format!("{}/analytics", domain))?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to analytics: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let analytics_response: DAnalyticsResponse = serde_json::from_str(&body_text)?;
Ok(analytics_response)
}
pub async fn usage(&self, domain: &str, auth: &Auth) -> Result<DAnalyticsResponse, SurgeError> {
let url = self.config.endpoint.join(&format!("{}/usage", domain))?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to usage: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let usage_response = serde_json::from_str(&body_text)?;
Ok(usage_response)
}
pub async fn audit(&self, domain: &str, auth: &Auth) -> Result<DAuditResponse, SurgeError> {
let url = self.config.endpoint.join(&format!("{}/audit", domain))?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to audit: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let audit_response = serde_json::from_str(&body_text)?;
Ok(audit_response)
}
pub async fn invite(
&self,
domain: &str,
emails: Value,
auth: &Auth,
) -> Result<bool, SurgeError> {
let url = self
.config
.endpoint
.join(&format!("{}/collaborators", domain))?;
let req = self.apply_auth(self.client.post(url), auth).json(&emails);
debug!("Request sent to invite: {:#?}", req);
let res = req.send().await?;
let status = res.status();
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
if status.is_success() {
Ok(true)
} else {
Ok(false)
}
}
pub async fn revoke(
&self,
domain: &str,
emails: Value,
auth: &Auth,
) -> Result<bool, SurgeError> {
let url = self
.config
.endpoint
.join(&format!("{}/collaborators", domain))?;
let req = self.apply_auth(self.client.delete(url), auth).json(&emails);
debug!("Request sent to revoke: {:#?}", req);
let res = req.send().await?;
let status = res.status();
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
if status.is_success() {
Ok(true)
} else {
Ok(false)
}
}
pub async fn plan(&self, plan: Value, auth: &Auth) -> Result<(), SurgeError> {
let url = self.config.endpoint.join("plan")?;
let req = self.apply_auth(self.client.put(url), auth).json(&plan);
debug!("Request sent to plan: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn card(&self, card: Value, auth: &Auth) -> Result<(), SurgeError> {
let url = self.config.endpoint.join("card")?;
let req = self.apply_auth(self.client.put(url), auth).json(&card);
debug!("Request sent to card: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub async fn plans(
&self,
domain: Option<&str>,
auth: &Auth,
) -> Result<PlansResponse, SurgeError> {
let path = match domain {
Some(d) => format!("{}/plans", d),
None => "plans".to_string(),
};
let url = self.config.endpoint.join(&path)?;
let req = self.apply_auth(self.client.get(url), auth);
debug!("Request sent to plans: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
let plans_response: PlansResponse = serde_json::from_str(&body_text)?;
Ok(plans_response)
}
pub async fn ssl(&self, domain: &str, pem_path: &Path, auth: &Auth) -> Result<(), SurgeError> {
let pem_data = fs::read(pem_path).map_err(|e| SurgeError::Io(e.to_string()))?;
let url = self.config.endpoint.join(&format!("{}/certs", domain))?;
let req = self.apply_auth(self.client.post(url), auth).body(pem_data);
debug!("Request sent to ssl: {:#?}", req);
let res = req.send().await?;
let body_text = res.text().await?;
debug!("response raw: {:?}", body_text);
Ok(())
}
pub fn apply_auth(&self, req: reqwest::RequestBuilder, auth: &Auth) -> reqwest::RequestBuilder {
match auth {
Auth::Token(token) => req.basic_auth("token", Some(token)),
Auth::UserPass { username, password } => req.basic_auth(username, Some(password)),
}
}
}