use self::encoder::Encoder;
use super::{rest::RestRequest, BuildStrategy};
use crate::error::SocketError;
use bytes::Bytes;
use hmac::Mac;
/// Implementations for encoding signatures generated by a [`RequestSigner`].
pub mod encoder;
/// API specific signing logic used by a [`RequestSigner`].
#[allow(clippy::needless_lifetimes)]
pub trait Signer {
/// Configuration required to sign the [`RestRequest`]s for this API server.
type Config<'a>
where
Self: 'a;
/// Generates a [`Self::Config`] for this [`RestRequest`] and
/// [`RequestBuilder`](reqwest::RequestBuilder).
///
/// # Examples
///
/// ## Private REST Request: FTX
/// ```rust,ignore
/// fn config<Request>(&self, _: Request, _: &RequestBuilder) -> Self::Config
/// where
/// Request: RestRequest
/// {
/// FtxSignConfig {
/// api_key: self.api_key.as_str(),
/// time: Utc::now(),
/// method: Request::method(),
/// path: Request::path()
/// }
/// }
/// ```
fn config<'a, Request>(
&'a self,
request: Request,
builder: &reqwest::RequestBuilder,
) -> Result<Self::Config<'a>, SocketError>
where
Request: RestRequest;
/// Generate the bytes to sign from the provided [`Self::Config`].
///
/// # Examples
///
/// ## Private REST Request: FTX
/// ```rust,ignore
/// fn bytes_to_sign(config: &Self::Config) -> Result<&[u8], SocketError> {
/// Ok(format!("{}{}{}", config.time, config.method, config.path).as_bytes())
/// }
/// ```
fn bytes_to_sign<'a>(config: &Self::Config<'a>) -> Bytes;
/// Build a signed [`reqwest::Request`] from the provided [`Self::Config`],
/// [`RequestBuilder`](reqwest::RequestBuilder), and generated cryptographic signature `String`.
///
/// # Examples
///
/// ## Private REST Request: FTX
/// ```rust,ignore
/// fn build_signed_request(config: Self::Config, builder: RequestBuilder, signature: String) -> Result<reqwest::Request, SocketError> {
/// // Add Ftx required Headers & build reqwest::Request
/// builder
/// .header(HEADER_FTX_KEY, &self.api_key)
/// .header(HEADER_FTX_SIGN, &signature)
/// .header(HEADER_FTX_TS, &time)
/// .build()
/// .map_err(SocketError::from)
/// }
/// ```
fn build_signed_request<'a>(
config: Self::Config<'a>,
builder: reqwest::RequestBuilder,
signature: String,
) -> Result<reqwest::Request, SocketError>;
}
/// Generically signs Http [`RestRequest`]s utilising API specific [`Signer`] logic, a hashable
/// [`Mac`], and a signature [`Encoder`].
#[derive(Debug, Copy, Clone)]
pub struct RequestSigner<Sig, Hmac, SigEncoder> {
signer: Sig,
mac: Hmac,
encoder: SigEncoder,
}
impl<Sig, Hmac, SigEncoder> BuildStrategy for RequestSigner<Sig, Hmac, SigEncoder>
where
Sig: Signer,
Hmac: Mac + Clone,
SigEncoder: Encoder,
{
fn build<Request>(
&self,
request: Request,
builder: reqwest::RequestBuilder,
) -> Result<reqwest::Request, SocketError>
where
Request: RestRequest,
{
// Build configuration required for generating signed requests
let config = self.signer.config(request, &builder)?;
// Generate bytes data used to update Mac state
let bytes_to_sign = Sig::bytes_to_sign(&config);
// Update Mac state & finalise bytes
let mut mac = self.mac.clone();
mac.update(&bytes_to_sign);
let bytes_to_encode = mac.finalize().into_bytes();
// Encode signature from Mac bytes
let signature = self.encoder.encode(bytes_to_encode);
Sig::build_signed_request(config, builder, signature)
}
}
impl<Sig, Hmac, SigEncoder> RequestSigner<Sig, Hmac, SigEncoder> {
/// Construct a new [`Self`] using the provided API specific configuration.
pub fn new(signer: Sig, mac: Hmac, encoder: SigEncoder) -> Self {
Self {
signer,
mac,
encoder,
}
}
}