#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod date;
mod encode;
mod error;
mod sign;
#[cfg(feature = "reqwest")]
mod reqwest_transport;
#[cfg(feature = "ureq")]
mod ureq_transport;
use std::time::{Duration, SystemTime};
pub use error::Error;
pub use sign::Credentials;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Addressing {
#[default]
Path,
VirtualHosted,
}
#[derive(Debug, Clone)]
pub struct Client {
scheme: String,
host: String,
bucket: String,
region: String,
credentials: Credentials,
addressing: Addressing,
}
impl Client {
pub fn new(
endpoint: &str,
bucket: &str,
region: &str,
credentials: Credentials,
) -> Result<Self, Error> {
let (scheme, rest) = endpoint
.split_once("://")
.ok_or_else(|| Error::Endpoint(endpoint.to_owned()))?;
if !matches!(scheme, "http" | "https") {
return Err(Error::Endpoint(endpoint.to_owned()));
}
let host = rest.trim_end_matches('/');
if host.is_empty() || host.contains('/') || host.contains('?') {
return Err(Error::Endpoint(endpoint.to_owned()));
}
let host = match (scheme, host.rsplit_once(':')) {
("http", Some((bare, "80"))) | ("https", Some((bare, "443"))) => bare,
_ => host,
};
if bucket.is_empty() {
return Err(Error::Bucket(bucket.to_owned()));
}
Ok(Self {
scheme: scheme.to_owned(),
host: host.to_owned(),
bucket: bucket.to_owned(),
region: region.to_owned(),
credentials,
addressing: Addressing::Path,
})
}
pub fn addressing(mut self, addressing: Addressing) -> Self {
self.addressing = addressing;
self
}
pub fn put(&self, key: &str, body: impl Into<Vec<u8>>) -> Request<'_> {
self.request("PUT", key, body.into())
}
pub fn get(&self, key: &str) -> Request<'_> {
self.request("GET", key, Vec::new())
}
pub fn head(&self, key: &str) -> Request<'_> {
self.request("HEAD", key, Vec::new())
}
pub fn delete(&self, key: &str) -> Request<'_> {
self.request("DELETE", key, Vec::new())
}
fn request(&self, method: &'static str, key: &str, body: Vec<u8>) -> Request<'_> {
Request {
client: self,
method,
key: key.to_owned(),
headers: Vec::new(),
query: Vec::new(),
body,
payload_sha256: None,
}
}
fn host_header(&self) -> String {
match self.addressing {
Addressing::Path => self.host.clone(),
Addressing::VirtualHosted => format!("{}.{}", self.bucket, self.host),
}
}
fn path(&self, key: &str) -> String {
let mut path = String::with_capacity(key.len() + self.bucket.len() + 2);
if self.addressing == Addressing::Path {
path.push('/');
path.push_str(&encode::uri(&self.bucket, false));
}
path.push('/');
path.push_str(&encode::uri(key, false));
path
}
}
#[derive(Debug)]
pub struct Request<'c> {
client: &'c Client,
method: &'static str,
key: String,
headers: Vec<(String, String)>,
query: Vec<(String, String)>,
body: Vec<u8>,
payload_sha256: Option<[u8; 32]>,
}
impl Request<'_> {
pub fn header(mut self, name: &str, value: &str) -> Self {
self.headers
.push((name.to_ascii_lowercase(), value.to_owned()));
self
}
pub fn query(mut self, name: &str, value: &str) -> Self {
self.query.push((name.to_owned(), value.to_owned()));
self
}
pub fn content_type(self, value: &str) -> Self {
self.header("content-type", value)
}
pub fn content_encoding(self, value: &str) -> Self {
self.header("content-encoding", value)
}
pub fn metadata(self, name: &str, value: &str) -> Self {
let name = format!("x-amz-meta-{}", name.to_ascii_lowercase());
self.header(&name, value)
}
pub fn range(self, start: u64, end: u64) -> Self {
let range = format!("bytes={start}-{end}");
self.header("range", &range)
}
pub fn payload_sha256(mut self, digest: [u8; 32]) -> Self {
self.payload_sha256 = Some(digest);
self
}
pub fn checksum_sha256(self, digest: [u8; 32]) -> Self {
let encoded = encode::base64(&digest);
self.payload_sha256(digest)
.header("x-amz-checksum-sha256", &encoded)
}
pub fn sign(self, at: SystemTime) -> SignedRequest {
let Request {
client,
method,
key,
headers,
query,
body,
payload_sha256,
} = self;
let path = client.path(&key);
let payload_hash = match payload_sha256 {
Some(digest) => encode::hex(&digest),
None => encode::hex(&sign::sha256(&body)),
};
let (mut signed, authorization) = sign::sign_headers(sign::HeaderSigning {
credentials: &client.credentials,
region: &client.region,
method,
path: &path,
query: &query,
host: &client.host_header(),
headers,
payload_hash: &payload_hash,
at,
});
signed.push(("authorization".to_owned(), authorization));
SignedRequest {
method,
url: url(client, &path, &query),
headers: signed,
body,
}
}
pub fn presign(self, at: SystemTime, expires: Duration) -> String {
let Request {
client,
method,
key,
query,
..
} = self;
let path = client.path(&key);
let (query, signature) = sign::presign_query(sign::QuerySigning {
credentials: &client.credentials,
region: &client.region,
method,
path: &path,
query: &query,
host: &client.host_header(),
at,
expires,
});
let mut presigned = url(client, &path, &query);
presigned.push_str("&X-Amz-Signature=");
presigned.push_str(&signature);
presigned
}
}
fn url(client: &Client, path: &str, query: &[(String, String)]) -> String {
let mut url = format!("{}://{}{}", client.scheme, client.host_header(), path);
if !query.is_empty() {
url.push('?');
url.push_str(&encode::query(query));
}
url
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedRequest {
pub method: &'static str,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}