Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions rustcloud/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,3 @@ serde_json = "1.0.118"
sha2 = "0.10.9"

tokio = { version = "1.37.0", features = ["full", "test-util"] }


[dev-dependencies]
rustcloud = { path = '.' }

[[language]]
name = "rust"
file-types = ["rs"]
comment-tokens = ["//", "///", "//!"]
indent = { tab-width = 2, unit = " " }
language-servers = [ "rust-analyzer"]
1 change: 0 additions & 1 deletion rustcloud/src/aws/aws_apis/compute/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pub mod aws_ec2;
pub mod aws_ecs;
pub mod aws_eks;
pub mod aws_paas;
2 changes: 0 additions & 2 deletions rustcloud/src/aws/aws_apis/database/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
pub mod aws_dynamodb;
pub mod aws_nosqlindexed;
pub mod aws_rbmds;
161 changes: 123 additions & 38 deletions rustcloud/src/azure/azure_apis/auth/azure_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,147 @@ use base64::{engine::general_purpose, Engine};
use chrono::Utc;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::collections::BTreeMap;
use std::env;

use crate::errors::CloudError;

type HmacSha256 = Hmac<Sha256>;
pub struct AzureAuth;


impl AzureAuth {
pub fn generate_headers(method: &str, account: &str, resource: &str) -> (String, String) {
pub fn generate_headers(
method: &str,
account: &str,
resource: &str,
) -> Result<(String, String), CloudError> {
let key = env::var("AZURE_STORAGE_KEY").map_err(|_| CloudError::Auth {
message: "AZURE_STORAGE_KEY not set".to_string(),
})?;

let key = env::var("AZURE_STORAGE_KEY").expect("AZURE_STORAGE_KEY not set");

let date = Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string();

let mut path = resource.to_string();

let mut query = String::new();

if let Some(pos) = resource.find('?') {
path = resource[..pos].to_string();

query = resource[pos + 1..].to_string();
}



let canonicalized_resource = if query.is_empty() {
format!("/{}{}", account, path)
} else {
let mut parts: Vec<&str> = query.split('=').collect();
format!(
"/{}{}\n{}:{}",
account,
path,
parts[0].to_lowercase(),
parts[1]
)
};
let canonicalized_resource = canonicalize_resource(account, resource)?;

let string_to_sign = format!(
"{}\n\n\n\n\n\n\n\n\n\n\n\nx-ms-date:{}\nx-ms-version:2020-10-02\n{}",
method, date, canonicalized_resource

);

let decoded_key = general_purpose::STANDARD.decode(key).unwrap();

let decoded_key =
general_purpose::STANDARD
.decode(key)
.map_err(|_| CloudError::Auth {
message: "AZURE_STORAGE_KEY is not valid base64".to_string(),
})?;

let mut mac = HmacSha256::new_from_slice(&decoded_key).map_err(|e| CloudError::Auth {
message: format!("invalid Azure signing key: {}", e),
})?;

let mut mac = HmacSha256::new_from_slice(&decoded_key).unwrap();

mac.update(string_to_sign.as_bytes());

let signature = general_purpose::STANDARD.encode(mac.finalize().into_bytes());


let signature = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
let auth_header = format!("SharedKey {}:{}", account, signature);

(auth_header, date)
Ok((auth_header, date))
}
}

fn canonicalize_resource(account: &str, resource: &str) -> Result<String, CloudError> {
let (path, query) = match resource.split_once('?') {
Some((path, query)) => (path, Some(query)),
None => (resource, None),
};

let mut canonicalized = format!("/{}{}", account, path);

if let Some(query) = query.filter(|query| !query.is_empty()) {
let mut params: BTreeMap<String, String> = BTreeMap::new();

for pair in query.split('&') {
if pair.is_empty() {
continue;
}

let (raw_key, raw_value) = pair.split_once('=').ok_or_else(|| CloudError::Auth {
message: format!("invalid Azure resource query parameter: {}", pair),
})?;

let key = raw_key.to_lowercase();
let value = raw_value.to_string();

params
.entry(key)
.and_modify(|existing| {
existing.push(',');
existing.push_str(&value);
})
.or_insert(value);
}

for (key, value) in params {
canonicalized.push('\n');
canonicalized.push_str(&format!("{}:{}", key, value));
}
}

Ok(canonicalized)
}

#[cfg(test)]
mod tests {
use super::AzureAuth;
use crate::errors::CloudError;
use std::sync::{Mutex, OnceLock};

static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

fn env_lock() -> &'static Mutex<()> {
ENV_LOCK.get_or_init(|| Mutex::new(()))
}

#[test]
fn generate_headers_fails_when_key_missing() {
let _guard = env_lock().lock().expect("env lock poisoned");

unsafe {
std::env::remove_var("AZURE_STORAGE_KEY");
}

let result = AzureAuth::generate_headers("GET", "account", "/?comp=list");

assert!(matches!(result, Err(CloudError::Auth { .. })));
}

#[test]
fn generate_headers_fails_when_key_not_base64() {
let _guard = env_lock().lock().expect("env lock poisoned");

unsafe {
std::env::set_var("AZURE_STORAGE_KEY", "not_base64");
}

let result = AzureAuth::generate_headers("GET", "account", "/?comp=list");

assert!(matches!(result, Err(CloudError::Auth { .. })));
}

#[test]
fn generate_headers_succeeds_with_valid_key() {
let _guard = env_lock().lock().expect("env lock poisoned");

unsafe {
std::env::set_var("AZURE_STORAGE_KEY", "MTIzNA==");
}

let result = AzureAuth::generate_headers(
"PUT",
"account",
"/container?restype=container&timeout=30",
)
.expect("expected valid auth header");

assert!(result.0.starts_with("SharedKey account:"));
assert!(!result.1.is_empty());
}
}
54 changes: 22 additions & 32 deletions rustcloud/src/azure/azure_apis/storage/azure_blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::error::Error;

use crate::azure::azure_apis::auth::azure_auth::AzureAuth;


pub struct AzureBlobClient {
client: Client,
account: String,
Expand All @@ -12,10 +11,8 @@ pub struct AzureBlobClient {

impl AzureBlobClient {
pub fn new(account: String) -> Self {

let base_url = format!("https://{}.blob.core.windows.net", account);


AzureBlobClient {
client: Client::new(),
account,
Expand All @@ -24,14 +21,12 @@ impl AzureBlobClient {
}

pub async fn list_containers(&self) -> Result<String, Box<dyn Error>> {

let resource = "/?comp=list";

let (auth, date) = AzureAuth::generate_headers("GET", &self.account, resource);


let (auth, date) = AzureAuth::generate_headers("GET", &self.account, resource)?;

let url = format!("{}?comp=list", self.base_url);

let response = self
.client
.get(&url)
Expand All @@ -40,26 +35,23 @@ impl AzureBlobClient {
.header("Authorization", auth)
.send()
.await?;

let status = response.status();
let body = response.text().await?;

if !status.is_success() {
return Err(format!("Azure error: {}", body).into());
}

Ok(body)

Ok(body)
}


pub async fn create_container(&self, container: &str) -> Result<String, Box<dyn Error>> {

let resource = format!("/{}?restype=container", container);
let (auth, date) = AzureAuth::generate_headers("PUT", &self.account, &resource);

let (auth, date) = AzureAuth::generate_headers("PUT", &self.account, &resource)?;
let url = format!("{}/{}?restype=container", self.base_url, container);

let response = self
.client
.put(&url)
Expand All @@ -69,27 +61,25 @@ impl AzureBlobClient {
.header("Authorization", auth)
.send()
.await?;

let status = response.status();

let body = response.text().await?;

if !status.is_success() {
return Err(format!("Azure error: {}", body).into());
}

Ok(body)
}



pub async fn delete_container(&self, container: &str) -> Result<String, Box<dyn Error>> {

let resource = format!("/{}?restype=container", container);
let (auth, date) = AzureAuth::generate_headers("DELETE", &self.account, &resource);

let (auth, date) = AzureAuth::generate_headers("DELETE", &self.account, &resource)?;

let url = format!("{}/{}?restype=container", self.base_url, container);

let response = self
.client
.delete(&url)
Expand All @@ -98,15 +88,15 @@ impl AzureBlobClient {
.header("Authorization", auth)
.send()
.await?;

let status = response.status();

let body = response.text().await?;

if !status.is_success() {
return Err(format!("Azure error: {}", body).into());
}

Ok(body)
}
}
9 changes: 6 additions & 3 deletions rustcloud/src/gcp/gcp_apis/storage/gcp_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,12 @@ impl GoogleStorage {
.send()
.await?;

let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
let mut response: HashMap<String, Value> = HashMap::new();
response.insert(
"status".to_string(),
Value::Number(resp.status().as_u16().into()),
Value::Number(status.into()),
);
response.insert("body".to_string(), Value::String(body));

Expand All @@ -149,12 +150,13 @@ impl GoogleStorage {
.send()
.await?;

let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();

let mut response = HashMap::new();
response.insert(
"status".to_string(),
Value::Number(resp.status().as_u16().into()),
Value::Number(status.into()),
);
response.insert("body".to_string(), Value::String(body));

Expand Down Expand Up @@ -243,12 +245,13 @@ impl GoogleStorage {
.send()
.await?;

let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();

let mut response = HashMap::new();
response.insert(
"status".to_string(),
Value::Number(resp.status().as_u16().into()),
Value::Number(status.into()),
);
response.insert("body".to_string(), Value::String(body));

Expand Down
3 changes: 1 addition & 2 deletions rustcloud/src/gcp/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
pub mod gcp_apis;
// pub m
pub mod types;
pub mod types;
10 changes: 10 additions & 0 deletions rustcloud/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pub mod aws;
pub mod azure;
pub mod digiocean;
pub mod errors;
pub mod gcp;
pub mod traits;
pub mod types;

#[cfg(test)]
mod tests;
Loading