use pink::chain_extension::HttpResponse;
use pink_extension as pink;
use scale::{Decode, Encode};
use base16;
use hmac::{Hmac, Mac};
use sha2::Digest;
use sha2::Sha256;
#[derive(Encode, Decode, Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
RequestFailed(u16),
InvalidEndpoint,
}
pub struct Head {
pub content_length: u64,
}
pub struct S3<'a> {
endpoint: &'a str,
region: &'a str,
host: String,
access_key: &'a str,
secret_key: &'a str,
}
impl<'a> S3<'a> {
pub fn new(
endpoint: &'a str,
region: &'a str,
access_key: &'a str,
secret_key: &'a str,
) -> Result<Self, Error> {
let https_scheme = "https://";
if !endpoint.starts_with(https_scheme) {
return Err(Error::InvalidEndpoint);
}
let host_start = https_scheme.len();
let host = endpoint[host_start..].to_owned();
if host.contains('/') {
return Err(Error::InvalidEndpoint);
}
Ok(Self {
endpoint,
region,
host,
access_key,
secret_key,
})
}
pub fn head(&self, bucket_name: &str, object_key: &str) -> Result<Head, Error> {
let response = self.request("HEAD", bucket_name, object_key, None)?;
for (k, v) in response.headers {
if k.to_ascii_lowercase() == "content-length" {
return Ok(Head {
content_length: v.parse().or(Err(Error::RequestFailed(600)))?,
});
}
}
Err(Error::RequestFailed(response.status_code))
}
pub fn get(&self, bucket_name: &str, object_key: &str) -> Result<Vec<u8>, Error> {
Ok(self.request("GET", bucket_name, object_key, None)?.body)
}
pub fn put(&self, bucket_name: &str, object_key: &str, value: &[u8]) -> Result<(), Error> {
self.request("PUT", bucket_name, object_key, Some(value))
.map(|_| ())
}
pub fn delete(&self, bucket_name: &str, object_key: &str) -> Result<(), Error> {
self.request("DELETE", bucket_name, object_key, None)
.map(|_| ())
}
fn request(
&self,
method: &str,
bucket_name: &str,
object_key: &str,
value: Option<&[u8]>,
) -> Result<HttpResponse, Error> {
let service = "s3";
let payload_hash = format!("{:x}", Sha256::digest(value.unwrap_or_default()));
let (datestamp, amz_date) = times();
let canonical_uri = format!("/{}/{}", bucket_name, object_key); let canonical_querystring = "";
let canonical_headers = format!(
"host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n",
self.host, payload_hash, amz_date
);
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
let canonical_request = format!(
"{}\n{}\n{}\n{}\n{}\n{}",
method,
canonical_uri,
canonical_querystring,
canonical_headers,
signed_headers,
payload_hash
);
let algorithm = "AWS4-HMAC-SHA256";
let credential_scope = format!("{}/{}/{}/aws4_request", datestamp, self.region, service);
let canonical_request_hash = format!("{:x}", Sha256::digest(&canonical_request.as_bytes()));
let string_to_sign = format!(
"{}\n{}\n{}\n{}",
algorithm, amz_date, credential_scope, canonical_request_hash
);
let signature_key = get_signature_key(
self.secret_key.as_bytes(),
datestamp.as_bytes(),
self.region.as_bytes(),
service.as_bytes(),
);
let signature_bytes = hmac_sign(&signature_key, &string_to_sign.as_bytes());
let signature = format!("{}", base16::encode_lower(&signature_bytes));
let authorization_header = format!(
"{} Credential={}/{}, SignedHeaders={}, Signature={}",
algorithm, self.access_key, credential_scope, signed_headers, signature
);
let mut headers: Vec<(String, String)> = vec![
("Authorization".into(), authorization_header),
("x-amz-content-sha256".into(), payload_hash),
("x-amz-date".into(), amz_date),
];
let body = if let Some(value) = value {
headers.push(("Content-Length".into(), format!("{}", &value.len())));
headers.push(("Content-Type".into(), "binary/octet-stream".into()));
value
} else {
&[]
};
let request_url = format!("{}/{}/{}", self.endpoint, bucket_name, object_key);
let response = pink::http_req!(method, request_url, body.to_vec(), headers);
if response.status_code / 100 != 2 {
return Err(Error::RequestFailed(response.status_code));
}
Ok(response)
}
}
fn times() -> (String, String) {
#[cfg(test)]
let datetime = chrono::Utc::now();
#[cfg(not(test))]
let datetime = {
use chrono::{TimeZone, Utc};
let time = pink::env().block_timestamp() / 1000;
Utc.timestamp(time.try_into().unwrap(), 0)
};
let datestamp = datetime.format("%Y%m%d").to_string();
let datetimestamp = datetime.format("%Y%m%dT%H%M%SZ").to_string();
(datestamp, datetimestamp)
}
type HmacSha256 = Hmac<Sha256>;
fn hmac_sign(key: &[u8], msg: &[u8]) -> Vec<u8> {
let mut mac =
<HmacSha256 as Mac>::new_from_slice(key).expect("Could not instantiate HMAC instance");
mac.update(msg);
let result = mac.finalize().into_bytes();
result.to_vec()
}
fn get_signature_key(
key: &[u8],
datestamp: &[u8],
region_name: &[u8],
service_name: &[u8],
) -> Vec<u8> {
let k_date = hmac_sign(&[b"AWS4", key].concat(), datestamp);
let k_region = hmac_sign(&k_date, region_name);
let k_service = hmac_sign(&k_region, service_name);
let k_signing = hmac_sign(&k_service, b"aws4_request");
return k_signing;
}
#[cfg(test)]
mod tests {
#[test]
#[ignore = "can not run concurrently"]
fn it_works() {
use crate as s3;
pink_extension_runtime::mock_ext::mock_all_ext();
let endpoint = "https://s3.kvin.wang:8443";
let region = "garage";
let access_key = "GKb36294dbfd49a894b19c20cb";
let secret_key = "c36c43f1ae5bcb27733753a633fb5df82cc57832822275a761d711637bb268d5";
let s3 = s3::S3::new(endpoint, region, access_key, secret_key).unwrap();
let bucket = "fat-1";
let object_key = "path/to/foo";
let value = b"bar";
s3.put(bucket, object_key, value).unwrap();
let head = s3.head(bucket, object_key).unwrap();
assert_eq!(head.content_length, value.len() as u64);
let v = s3.get(bucket, object_key).unwrap();
assert_eq!(v, value);
s3.delete(bucket, object_key).unwrap();
}
}