use std::fmt;
use std::net::SocketAddr;
use crate::ClientError;
use cheetah_string::CheetahString;
use rocketmq_auth::cal_signature_segments_with_algorithm;
use rocketmq_auth::SignatureAlgorithm;
use rocketmq_error::REDACTED;
use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
use rocketmq_transport::api::RPCHook;
use smallvec::SmallVec;
use crate::common::session_credentials::SessionCredentials;
use crate::common::session_credentials::ACCESS_KEY;
use crate::common::session_credentials::SECURITY_TOKEN;
use crate::common::session_credentials::SIGNATURE;
#[derive(Clone)]
pub struct AclClientRPCHook {
session_credentials: SessionCredentials,
signature_algorithm: SignatureAlgorithm,
}
impl fmt::Debug for AclClientRPCHook {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AclClientRPCHook")
.field("session_credentials", &REDACTED)
.field("signature_algorithm", &self.signature_algorithm)
.finish()
}
}
impl AclClientRPCHook {
pub fn new(session_credentials: SessionCredentials) -> Self {
Self::with_signature_algorithm(session_credentials, SignatureAlgorithm::default())
}
pub fn with_signature_algorithm(
session_credentials: SessionCredentials,
signature_algorithm: SignatureAlgorithm,
) -> Self {
Self {
session_credentials,
signature_algorithm,
}
}
pub fn session_credentials(&self) -> &SessionCredentials {
&self.session_credentials
}
fn sorted_ext_fields_for_signing(request: &RemotingCommand) -> SmallVec<[(&CheetahString, &CheetahString); 16]> {
let mut sorted_fields = SmallVec::<[(&CheetahString, &CheetahString); 16]>::new();
if let Some(ext_fields) = request.ext_fields() {
for (key, value) in ext_fields {
sorted_fields.push((key, value));
}
}
sorted_fields.sort_unstable_by(|(left, _), (right, _)| left.as_str().cmp(right.as_str()));
sorted_fields
}
}
impl RPCHook for AclClientRPCHook {
fn do_before_request(
&self,
_remote_addr: SocketAddr,
request: &mut RemotingCommand,
) -> Result<(), rocketmq_error::SharedError> {
let access_key = self
.session_credentials
.access_key()
.filter(|value| !value.is_empty())
.ok_or_else(|| ClientError::illegal_argument("ACL AccessKey must not be blank").into_shared_error())?;
let secret_key = self
.session_credentials
.secret_key()
.filter(|value| !value.is_empty())
.ok_or_else(|| ClientError::illegal_argument("ACL SecretKey must not be blank").into_shared_error())?;
request.ensure_ext_fields_initialized();
request.add_ext_field(ACCESS_KEY, access_key.clone());
if let Some(security_token) = self.session_credentials.security_token() {
request.add_ext_field(SECURITY_TOKEN, security_token.clone());
}
request.make_custom_header_to_net();
let signature = {
let sorted_fields = Self::sorted_ext_fields_for_signing(request);
let field_segments = sorted_fields
.iter()
.filter(|(key, _)| key.as_str() != SIGNATURE)
.map(|(_, value)| value.as_bytes());
let body_segments = request.body().into_iter().map(|body| body.as_ref());
cal_signature_segments_with_algorithm(
field_segments.chain(body_segments),
secret_key.as_str(),
self.signature_algorithm,
)
.map_err(|source| ClientError::illegal_argument_source(source).into_shared_error())?
};
request.add_ext_field(SIGNATURE, signature);
Ok(())
}
fn do_after_response(
&self,
_remote_addr: SocketAddr,
_request: &RemotingCommand,
_response: &mut RemotingCommand,
) -> Result<(), rocketmq_error::SharedError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use cheetah_string::CheetahString;
use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
use super::*;
#[test]
fn acl_hook_signs_sorted_ext_fields_and_body_like_java_client() {
let hook = AclClientRPCHook::new(SessionCredentials::with_token("ak", "secret", "token"));
let mut request = RemotingCommand::create_remoting_command(10).set_body(Bytes::from_static(b"body"));
request.ensure_ext_fields_initialized();
request.add_ext_field("z", "last");
request.add_ext_field("a", "first");
hook.do_before_request("127.0.0.1:9876".parse().unwrap(), &mut request)
.unwrap();
let ext_fields = request.ext_fields().unwrap();
assert_eq!(
ext_fields.get(&CheetahString::from_static_str(ACCESS_KEY)).unwrap(),
"ak"
);
assert_eq!(
ext_fields.get(&CheetahString::from_static_str(SECURITY_TOKEN)).unwrap(),
"token"
);
assert_eq!(
ext_fields.get(&CheetahString::from_static_str(SIGNATURE)).unwrap(),
"wuJkFDNRMKY6aFXTDwT/vg0UC8M="
);
}
#[test]
fn acl_hook_excludes_existing_signature_when_resigning_like_java_client() {
let hook = AclClientRPCHook::new(SessionCredentials::with_token("ak", "secret", "token"));
let mut request = RemotingCommand::create_remoting_command(10).set_body(Bytes::from_static(b"body"));
request.ensure_ext_fields_initialized();
request.add_ext_field("z", "last");
request.add_ext_field("a", "first");
request.add_ext_field(SIGNATURE, "stale-signature");
hook.do_before_request("127.0.0.1:9876".parse().unwrap(), &mut request)
.unwrap();
let ext_fields = request.ext_fields().unwrap();
assert_eq!(
ext_fields.get(&CheetahString::from_static_str(SIGNATURE)).unwrap(),
"wuJkFDNRMKY6aFXTDwT/vg0UC8M="
);
}
#[test]
fn acl_hook_supports_hmac_sha256_for_secure_profiles() {
let hook = AclClientRPCHook::with_signature_algorithm(
SessionCredentials::with_token("ak", "secret", "token"),
SignatureAlgorithm::HmacSha256,
);
let mut request = RemotingCommand::create_remoting_command(10).set_body(Bytes::from_static(b"body"));
request.ensure_ext_fields_initialized();
request.add_ext_field("z", "last");
request.add_ext_field("a", "first");
hook.do_before_request("127.0.0.1:9876".parse().unwrap(), &mut request)
.unwrap();
assert_eq!(
request
.ext_fields()
.unwrap()
.get(&CheetahString::from_static_str(SIGNATURE))
.unwrap(),
"mBwsFNM/fUVM2NmLpnXIS+x+x7EZXmyuboG/4ZGmwa0="
);
}
#[test]
fn acl_hook_rejects_blank_credentials_before_signing() {
let hook = AclClientRPCHook::new(SessionCredentials::default());
let mut request = RemotingCommand::create_remoting_command(10);
assert!(hook
.do_before_request("127.0.0.1:9876".parse().unwrap(), &mut request)
.is_err());
}
#[test]
fn acl_hook_debug_redacts_session_credentials() {
let hook = AclClientRPCHook::new(SessionCredentials::with_token("ak", "secret", "token"));
let debug = format!("{hook:?}");
assert!(debug.contains(REDACTED));
assert!(!debug.contains("ak"));
assert!(!debug.contains("secret"));
assert!(!debug.contains("token"));
}
}