From eb35be48d3f7eba38bfa79f743a1abedad3b3732 Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Fri, 17 May 2019 17:28:20 -0700 Subject: [PATCH] Implement get_bucket_location API - with unsigned body --- Cargo.toml | 4 ++ src/main.rs | 24 ++++++- src/minio.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++-- src/minio/api.rs | 27 ++++++++ src/minio/sign.rs | 45 +++++++------- 5 files changed, 225 insertions(+), 30 deletions(-) create mode 100644 src/minio/api.rs 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={}",