rocketmq_client_rust/common/
acl_client_rpc_hook.rs1use std::fmt;
16use std::net::SocketAddr;
17
18use crate::ClientError;
19use cheetah_string::CheetahString;
20use rocketmq_auth::cal_signature_segments_with_algorithm;
21use rocketmq_auth::SignatureAlgorithm;
22use rocketmq_error::REDACTED;
23use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
24use rocketmq_transport::api::RPCHook;
25use smallvec::SmallVec;
26
27use crate::common::session_credentials::SessionCredentials;
28use crate::common::session_credentials::ACCESS_KEY;
29use crate::common::session_credentials::SECURITY_TOKEN;
30use crate::common::session_credentials::SIGNATURE;
31
32#[derive(Clone)]
39pub struct AclClientRPCHook {
40 session_credentials: SessionCredentials,
41 signature_algorithm: SignatureAlgorithm,
42}
43
44impl fmt::Debug for AclClientRPCHook {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 formatter
47 .debug_struct("AclClientRPCHook")
48 .field("session_credentials", &REDACTED)
49 .field("signature_algorithm", &self.signature_algorithm)
50 .finish()
51 }
52}
53
54impl AclClientRPCHook {
55 pub fn new(session_credentials: SessionCredentials) -> Self {
56 Self::with_signature_algorithm(session_credentials, SignatureAlgorithm::default())
57 }
58
59 pub fn with_signature_algorithm(
60 session_credentials: SessionCredentials,
61 signature_algorithm: SignatureAlgorithm,
62 ) -> Self {
63 Self {
64 session_credentials,
65 signature_algorithm,
66 }
67 }
68
69 pub fn session_credentials(&self) -> &SessionCredentials {
70 &self.session_credentials
71 }
72
73 fn sorted_ext_fields_for_signing(request: &RemotingCommand) -> SmallVec<[(&CheetahString, &CheetahString); 16]> {
74 let mut sorted_fields = SmallVec::<[(&CheetahString, &CheetahString); 16]>::new();
75 if let Some(ext_fields) = request.ext_fields() {
76 for (key, value) in ext_fields {
77 sorted_fields.push((key, value));
78 }
79 }
80 sorted_fields.sort_unstable_by(|(left, _), (right, _)| left.as_str().cmp(right.as_str()));
81 sorted_fields
82 }
83}
84
85impl RPCHook for AclClientRPCHook {
86 fn do_before_request(
87 &self,
88 _remote_addr: SocketAddr,
89 request: &mut RemotingCommand,
90 ) -> Result<(), rocketmq_error::SharedError> {
91 let access_key = self
92 .session_credentials
93 .access_key()
94 .filter(|value| !value.is_empty())
95 .ok_or_else(|| ClientError::illegal_argument("ACL AccessKey must not be blank").into_shared_error())?;
96 let secret_key = self
97 .session_credentials
98 .secret_key()
99 .filter(|value| !value.is_empty())
100 .ok_or_else(|| ClientError::illegal_argument("ACL SecretKey must not be blank").into_shared_error())?;
101
102 request.ensure_ext_fields_initialized();
103 request.add_ext_field(ACCESS_KEY, access_key.clone());
104 if let Some(security_token) = self.session_credentials.security_token() {
105 request.add_ext_field(SECURITY_TOKEN, security_token.clone());
106 }
107
108 request.make_custom_header_to_net();
109 let signature = {
110 let sorted_fields = Self::sorted_ext_fields_for_signing(request);
111 let field_segments = sorted_fields
112 .iter()
113 .filter(|(key, _)| key.as_str() != SIGNATURE)
114 .map(|(_, value)| value.as_bytes());
115 let body_segments = request.body().into_iter().map(|body| body.as_ref());
116
117 cal_signature_segments_with_algorithm(
118 field_segments.chain(body_segments),
119 secret_key.as_str(),
120 self.signature_algorithm,
121 )
122 .map_err(|source| ClientError::illegal_argument_source(source).into_shared_error())?
123 };
124 request.add_ext_field(SIGNATURE, signature);
125
126 Ok(())
127 }
128
129 fn do_after_response(
130 &self,
131 _remote_addr: SocketAddr,
132 _request: &RemotingCommand,
133 _response: &mut RemotingCommand,
134 ) -> Result<(), rocketmq_error::SharedError> {
135 Ok(())
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use bytes::Bytes;
142 use cheetah_string::CheetahString;
143 use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
144
145 use super::*;
146
147 #[test]
148 fn acl_hook_signs_sorted_ext_fields_and_body_like_java_client() {
149 let hook = AclClientRPCHook::new(SessionCredentials::with_token("ak", "secret", "token"));
150 let mut request = RemotingCommand::create_remoting_command(10).set_body(Bytes::from_static(b"body"));
151 request.ensure_ext_fields_initialized();
152 request.add_ext_field("z", "last");
153 request.add_ext_field("a", "first");
154
155 hook.do_before_request("127.0.0.1:9876".parse().unwrap(), &mut request)
156 .unwrap();
157
158 let ext_fields = request.ext_fields().unwrap();
159 assert_eq!(
160 ext_fields.get(&CheetahString::from_static_str(ACCESS_KEY)).unwrap(),
161 "ak"
162 );
163 assert_eq!(
164 ext_fields.get(&CheetahString::from_static_str(SECURITY_TOKEN)).unwrap(),
165 "token"
166 );
167 assert_eq!(
168 ext_fields.get(&CheetahString::from_static_str(SIGNATURE)).unwrap(),
169 "wuJkFDNRMKY6aFXTDwT/vg0UC8M="
170 );
171 }
172
173 #[test]
174 fn acl_hook_excludes_existing_signature_when_resigning_like_java_client() {
175 let hook = AclClientRPCHook::new(SessionCredentials::with_token("ak", "secret", "token"));
176 let mut request = RemotingCommand::create_remoting_command(10).set_body(Bytes::from_static(b"body"));
177 request.ensure_ext_fields_initialized();
178 request.add_ext_field("z", "last");
179 request.add_ext_field("a", "first");
180 request.add_ext_field(SIGNATURE, "stale-signature");
181
182 hook.do_before_request("127.0.0.1:9876".parse().unwrap(), &mut request)
183 .unwrap();
184
185 let ext_fields = request.ext_fields().unwrap();
186 assert_eq!(
187 ext_fields.get(&CheetahString::from_static_str(SIGNATURE)).unwrap(),
188 "wuJkFDNRMKY6aFXTDwT/vg0UC8M="
189 );
190 }
191
192 #[test]
193 fn acl_hook_supports_hmac_sha256_for_secure_profiles() {
194 let hook = AclClientRPCHook::with_signature_algorithm(
195 SessionCredentials::with_token("ak", "secret", "token"),
196 SignatureAlgorithm::HmacSha256,
197 );
198 let mut request = RemotingCommand::create_remoting_command(10).set_body(Bytes::from_static(b"body"));
199 request.ensure_ext_fields_initialized();
200 request.add_ext_field("z", "last");
201 request.add_ext_field("a", "first");
202
203 hook.do_before_request("127.0.0.1:9876".parse().unwrap(), &mut request)
204 .unwrap();
205
206 assert_eq!(
207 request
208 .ext_fields()
209 .unwrap()
210 .get(&CheetahString::from_static_str(SIGNATURE))
211 .unwrap(),
212 "mBwsFNM/fUVM2NmLpnXIS+x+x7EZXmyuboG/4ZGmwa0="
213 );
214 }
215
216 #[test]
217 fn acl_hook_rejects_blank_credentials_before_signing() {
218 let hook = AclClientRPCHook::new(SessionCredentials::default());
219 let mut request = RemotingCommand::create_remoting_command(10);
220
221 assert!(hook
222 .do_before_request("127.0.0.1:9876".parse().unwrap(), &mut request)
223 .is_err());
224 }
225
226 #[test]
227 fn acl_hook_debug_redacts_session_credentials() {
228 let hook = AclClientRPCHook::new(SessionCredentials::with_token("ak", "secret", "token"));
229
230 let debug = format!("{hook:?}");
231
232 assert!(debug.contains(REDACTED));
233 assert!(!debug.contains("ak"));
234 assert!(!debug.contains("secret"));
235 assert!(!debug.contains("token"));
236 }
237}