diff --git a/Cargo.toml b/Cargo.toml
index 17b51c1..0bda4e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,3 +8,7 @@ edition = "2018"
hyper = "0.12.28"
ring = "0.14.6"
time = "0.1.42"
+http = "0.1.17"
+hyper-tls = "0.3.2"
+futures = "0.1.27"
+bytes = "0.4.12"
diff --git a/src/main.rs b/src/main.rs
index 33b7e18..4034743 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,24 @@
mod minio;
-fn main() {
- // let r = minio::Settings::new("localhost:9000", "minio", "minio123");
- // println!("{:?}", r)
+use futures::future::Future;
+use hyper::rt;
+
+fn get_local_default_server() -> minio::Client {
+ match minio::Client::new("http://localhost:9000") {
+ Ok(mut c) => {
+ c.set_credentials(minio::Credentials::new("minio", "minio123"));
+ c
+ }
+ Err(_) => panic!("could not make local client"),
+ }
+}
+
+fn main() {
+ rt::run(rt::lazy(|| {
+ // let c = get_local_default_server();
+ let c = minio::Client::get_play_client();
+ c.get_bucket_location("txp")
+ .map(|res| println!("{}", res))
+ .map_err(|err| println!("{:?}", err))
+ }));
}
diff --git a/src/minio.rs b/src/minio.rs
index 2133778..8894eaf 100644
--- a/src/minio.rs
+++ b/src/minio.rs
@@ -1,8 +1,18 @@
+mod api;
mod sign;
-use hyper::{body::Body, header::HeaderMap, Method, Uri};
+use bytes::Bytes;
+use futures::future::{self, Future};
+use futures::stream::Stream;
+use http;
+use hyper::header::{HeaderName, HeaderValue};
+use hyper::{body::Body, client, header, header::HeaderMap, Method, Uri};
+use hyper_tls::HttpsConnector;
use std::collections::HashMap;
-use std::{env, string::String};
+use std::env;
+use std::sync::Arc;
+use std::{string, string::String};
+use time;
use time::Tm;
#[derive(Debug, Clone)]
@@ -36,12 +46,40 @@ pub type Region = String;
pub enum Err {
InvalidUrl(String),
InvalidEnv(String),
+ HttpErr(http::Error),
+ HyperErr(hyper::Error),
+ FailStatusCodeErr(hyper::StatusCode, Bytes),
+ Utf8DecodingErr(string::FromUtf8Error),
+}
+
+trait ApiClient {
+ fn make_req(&self, req: http::Request
) -> client::ResponseFuture;
+}
+
+struct HttpApiClient {
+ c: client::Client,
+}
+
+impl ApiClient for HttpApiClient {
+ fn make_req(&self, req: http::Request) -> client::ResponseFuture {
+ self.c.request(req)
+ }
+}
+
+struct HttpsApiClient {
+ c: client::Client, Body>,
+}
+
+impl ApiClient for HttpsApiClient {
+ fn make_req(&self, req: http::Request) -> client::ResponseFuture {
+ self.c.request(req)
+ }
}
-#[derive(Debug)]
pub struct Client {
server: Uri,
region: Region,
+ conn_client: Arc>,
pub credentials: Option,
}
@@ -56,8 +94,18 @@ impl Client {
Err(Err::InvalidUrl("invalid scheme!".to_string()))
} else {
Ok(Client {
- server: s,
+ server: s.clone(),
region: String::from(""),
+ conn_client: if s.scheme_str() == Some("http") {
+ Arc::new(Box::new(HttpApiClient {
+ c: client::Client::new(),
+ }))
+ } else {
+ let https = HttpsConnector::new(4).unwrap();
+ Arc::new(Box::new(HttpsApiClient {
+ c: client::Client::builder().build::<_, hyper::Body>(https),
+ }))
+ },
credentials: None,
})
}
@@ -74,16 +122,90 @@ impl Client {
self.region = r;
}
+ fn add_host_header(&self, h: &mut HeaderMap) {
+ let host_val = match self.server.port_part() {
+ Some(port) => format!("{}:{}", self.server.host().unwrap_or(""), port),
+ None => self.server.host().unwrap_or("").to_string(),
+ };
+ match header::HeaderValue::from_str(&host_val) {
+ Ok(v) => {
+ h.insert(header::HOST, v);
+ }
+ _ => {}
+ }
+ }
+
pub fn get_play_client() -> Client {
Client {
server: "https://play.min.io:9000".parse::().unwrap(),
region: String::from(""),
+ conn_client: {
+ let https = HttpsConnector::new(4).unwrap();
+ Arc::new(Box::new(HttpsApiClient {
+ c: client::Client::builder().build::<_, hyper::Body>(https),
+ }))
+ },
credentials: Some(Credentials::new(
"Q3AM3UQ867SPQQA43P2F",
"zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
)),
}
}
+
+ pub fn get_bucket_location(&self, b: &str) -> impl Future- {
+ let mut qp = HashMap::new();
+ qp.insert("location".to_string(), None);
+ let mut hmap = HeaderMap::new();
+
+ self.add_host_header(&mut hmap);
+ let body_hash_hdr = (
+ HeaderName::from_static("x-amz-content-sha256"),
+ HeaderValue::from_static("UNSIGNED-PAYLOAD"),
+ );
+ hmap.insert(body_hash_hdr.0.clone(), body_hash_hdr.1.clone());
+ let s3_req = S3Req {
+ method: Method::GET,
+ bucket: Some(b.to_string()),
+ object: None,
+ headers: hmap,
+ query: qp,
+ body: Body::empty(),
+ ts: time::now_utc(),
+ };
+
+ let sign_hdrs = sign::sign_v4(&s3_req, &self);
+ println!("signout: {:?}", sign_hdrs);
+ let req_result = api::mk_request(&s3_req, &self, &sign_hdrs);
+ let conn_client = Arc::clone(&self.conn_client);
+ future::result(req_result)
+ .map_err(|e| Err::HttpErr(e))
+ .and_then(move |req| {
+ conn_client
+ .make_req(req)
+ .map_err(|e| Err::HyperErr(e))
+ .and_then(|resp| {
+ let st = resp.status();
+ resp.into_body()
+ .concat2()
+ .map_err(|err| Err::HyperErr(err))
+ .map(move |chunk| (st, chunk.into_bytes()))
+ })
+ .and_then(|(code, body)| {
+ if code.is_success() {
+ b2s(body)
+ } else {
+ Err(Err::FailStatusCodeErr(code, body))
+ }
+ })
+ })
+ }
+}
+
+fn b2s(b: Bytes) -> Result {
+ match String::from_utf8(b.iter().map(|x| x.clone()).collect::>()) {
+ Err(e) => Err(Err::Utf8DecodingErr(e)),
+ Ok(s) => Ok(s),
+ }
}
pub struct S3Req {
@@ -95,3 +217,28 @@ pub struct S3Req {
body: Body,
ts: Tm,
}
+
+impl S3Req {
+ fn mk_path(&self) -> String {
+ let mut res: String = String::from("/");
+ if let Some(s) = &self.bucket {
+ res.push_str(&s);
+ res.push_str("/");
+ if let Some(o) = &self.object {
+ res.push_str(&o);
+ }
+ };
+ res
+ }
+
+ fn mk_query(&self) -> String {
+ self.query
+ .iter()
+ .map(|(x, y)| match y {
+ Some(v) => format!("{}={}", x, v),
+ None => x.to_string(),
+ })
+ .collect::>()
+ .join("&")
+ }
+}
diff --git a/src/minio/api.rs b/src/minio/api.rs
new file mode 100644
index 0000000..51764ee
--- /dev/null
+++ b/src/minio/api.rs
@@ -0,0 +1,27 @@
+use crate::minio;
+use http;
+use hyper::{header::HeaderName, header::HeaderValue, Body, Request};
+
+pub fn mk_request(
+ r: &minio::S3Req,
+ c: &minio::Client,
+ sign_hdrs: &Vec<(HeaderName, HeaderValue)>,
+) -> http::Result> {
+ let mut request = Request::builder();
+ let svr_str = &c.server.to_string();
+ let uri_str = svr_str.trim_end_matches('/');
+ println!("uri_str: {}", uri_str);
+ let upd_uri = format!("{}{}?{}", uri_str, r.mk_path(), r.mk_query());
+ println!("upd_uri: {}", upd_uri);
+
+ request.uri(&upd_uri).method(&r.method);
+ for hdr in r
+ .headers
+ .iter()
+ .map(|(x, y)| (x.clone(), y.clone()))
+ .chain(sign_hdrs.iter().map(|x| x.clone()))
+ {
+ request.header(hdr.0, hdr.1);
+ }
+ request.body(Body::empty())
+}
diff --git a/src/minio/sign.rs b/src/minio/sign.rs
index 77b4e6e..659e72b 100644
--- a/src/minio/sign.rs
+++ b/src/minio/sign.rs
@@ -11,6 +11,10 @@ fn aws_format_time(t: &Tm) -> String {
t.strftime("%Y%m%dT%H%M%SZ").unwrap().to_string()
}
+fn aws_format_date(t: &Tm) -> String {
+ t.strftime("%Y%m%d").unwrap().to_string()
+}
+
fn mk_scope(t: &Tm, r: &minio::Region) -> String {
let scope_time = t.strftime("%Y%m%d").unwrap().to_string();
format!("{}/{}/s3/aws4_request", scope_time, r)
@@ -53,7 +57,7 @@ fn uri_encode(c: char, encode_slash: bool) -> String {
} else {
let mut b = [0; 8];
let cs = c.encode_utf8(&mut b).as_bytes();
- cs.iter().map(|x| format!("%{:X}", x)).collect()
+ cs.iter().map(|x| format!("%{:02X}", x)).collect()
}
}
@@ -66,32 +70,23 @@ fn get_canonical_querystr(q: &HashMap>) -> String {
hs.sort();
let vs: Vec = hs
.drain(..)
- .map(|(x, y)| match y {
- Some(s) => uri_encode_str(&x, true) + "=" + &uri_encode_str(&s, true),
- None => uri_encode_str(&x, true),
+ .map(|(x, y)| {
+ let val_str = match y {
+ Some(s) => uri_encode_str(&s, true),
+ None => "".to_string(),
+ };
+ uri_encode_str(&x, true) + "=" + &val_str
})
.collect();
vs[..].join("&")
}
-fn mk_path(r: &minio::S3Req) -> String {
- let mut res: String = String::from("");
- if let Some(s) = &r.bucket {
- res.push_str(&s);
- if let Some(o) = &r.object {
- let s1 = format!("/{}", o);
- res.push_str(&s1);
- }
- };
- res
-}
-
fn get_canonical_request(
r: &minio::S3Req,
hdrs_to_use: &Vec<(String, String)>,
signed_hdrs_str: &str,
) -> String {
- let path_str = mk_path(r);
+ let path_str = r.mk_path();
let canonical_qstr = get_canonical_querystr(&r.query);
let canonical_hdrs: String = hdrs_to_use
.iter()
@@ -116,7 +111,7 @@ fn string_to_sign(ts: &Tm, scope: &str, canonical_request: &str) -> String {
let sha256_digest: String = digest::digest(&digest::SHA256, canonical_request.as_bytes())
.as_ref()
.iter()
- .map(|x| format!("{:X}", x))
+ .map(|x| format!("{:02x}", x))
.collect();
vec![
"AWS4-HMAC-SHA256",
@@ -134,7 +129,7 @@ fn hmac_sha256(msg: &str, key: &[u8]) -> hmac::Signature {
fn get_signing_key(ts: &Tm, region: &str, secret_key: &str) -> Vec {
let kstr = format!("AWS4{}", secret_key);
- let s1 = hmac_sha256(&aws_format_time(&ts), kstr.as_bytes());
+ let s1 = hmac_sha256(&aws_format_date(&ts), kstr.as_bytes());
let s2 = hmac_sha256(®ion, s1.as_ref());
let s3 = hmac_sha256("s3", s2.as_ref());
let s4 = hmac_sha256("aws4_request", s3.as_ref());
@@ -144,12 +139,11 @@ fn get_signing_key(ts: &Tm, region: &str, secret_key: &str) -> Vec {
fn compute_sign(str_to_sign: &str, key: &Vec) -> String {
let s1 = hmac_sha256(&str_to_sign, key.as_slice());
- s1.as_ref().iter().map(|x| format!("{:X}", x)).collect()
+ s1.as_ref().iter().map(|x| format!("{:02x}", x)).collect()
}
-pub fn sign_v4(r: &minio::S3Req, c: &minio::Client) -> Option> {
- let creds = c.credentials.clone();
- creds.map(|creds| {
+pub fn sign_v4(r: &minio::S3Req, c: &minio::Client) -> Vec<(HeaderName, HeaderValue)> {
+ c.credentials.clone().map_or(Vec::new(), |creds| {
let scope = mk_scope(&r.ts, &c.region);
let date_hdr = (
HeaderName::from_static("x-amz-date"),
@@ -157,6 +151,7 @@ pub fn sign_v4(r: &minio::S3Req, c: &minio::Client) -> Option Option>()
.join(";");
let cr = get_canonical_request(r, &hs, &signed_hdrs_str);
+ println!("canonicalreq: {}", cr);
let s2s = string_to_sign(&r.ts, &scope, &cr);
+ println!("s2s: {}", s2s);
let skey = get_signing_key(&r.ts, &c.region, &creds.secret_key);
+ println!("skey: {:?}", skey);
let signature = compute_sign(&s2s, &skey);
+ println!("sign: {}", signature);
let auth_hdr_val = format!(
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",