use std::collections::BTreeMap;
use base64::Engine;
use cheetah_string::CheetahString;
use rocketmq_common::common::mix_all::UNIQUE_MSG_QUERY_FLAG;
use rocketmq_common::common::mq_version::RocketMqVersion;
use rocketmq_error::AuthError;
use rocketmq_remoting::protocol::remoting_command::RemotingCommand;
#[cfg(feature = "grpc")]
use tracing::warn;
use crate::authentication::builder::AuthenticationContextBuilder;
use crate::authentication::context::default_authentication_context::DefaultAuthenticationContext;
const CREDENTIAL: &str = "Credential";
const SIGNATURE: &str = "Signature";
const ACCESS_KEY: &str = "AccessKey";
const SIGNATURE_FIELD: &str = "Signature";
const SPACE: char = ' ';
const COMMA: char = ',';
const EQUAL: char = '=';
const SLASH: char = '/';
#[derive(Debug, Default, Clone, Copy)]
pub struct DefaultAuthenticationContextBuilder;
impl DefaultAuthenticationContextBuilder {
pub fn new() -> Self {
Self
}
fn hex_to_base64(hex_input: &str) -> Result<String, AuthError> {
let bytes = hex::decode(hex_input)
.map_err(|e| AuthError::InvalidHexSignature(format!("Failed to decode hex: {}", e)))?;
Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
}
fn combine_request_content(
request: &RemotingCommand,
sorted_fields: &BTreeMap<CheetahString, CheetahString>,
) -> Vec<u8> {
let mut content = Vec::new();
for (key, value) in sorted_fields {
content.extend_from_slice(key.as_bytes());
content.extend_from_slice(value.as_bytes());
}
if let Some(body) = request.body() {
content.extend_from_slice(body);
}
content
}
}
impl AuthenticationContextBuilder<DefaultAuthenticationContext> for DefaultAuthenticationContextBuilder {
#[cfg(feature = "grpc")]
fn build_from_grpc(
&self,
metadata: &tonic::metadata::MetadataMap,
request: &dyn std::any::Any,
) -> Result<DefaultAuthenticationContext, AuthError> {
let mut context = DefaultAuthenticationContext::new();
if let Some(channel_id) = metadata.get("channel-id") {
if let Ok(channel_id_str) = channel_id.to_str() {
context.base.set_channel_id(Some(CheetahString::from(channel_id_str)));
}
}
let rpc_code = std::any::type_name_of_val(request);
context.base.set_rpc_code(Some(CheetahString::from(rpc_code)));
let authorization = metadata
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if authorization.is_empty() {
return Ok(context);
}
let datetime = metadata
.get("x-mq-date-time")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| AuthError::MissingDateTime("datetime header is required".into()))?;
let parts: Vec<&str> = authorization.splitn(2, SPACE).collect();
if parts.len() != 2 {
return Err(AuthError::InvalidAuthorizationHeader(
"authentication header format is incorrect".into(),
));
}
let key_values: Vec<&str> = parts[1].split(COMMA).collect();
for key_value in key_values {
let kv: Vec<&str> = key_value.trim().splitn(2, EQUAL).collect();
if kv.len() != 2 {
warn!("Skipping invalid authentication key-value pair: {}", key_value);
continue;
}
let auth_item = kv[0];
let value = kv[1];
match auth_item {
CREDENTIAL => {
let credential_parts: Vec<&str> = value.split(SLASH).collect();
if credential_parts.is_empty() {
return Err(AuthError::InvalidCredential("credential is empty".into()));
}
context.set_username(CheetahString::from(credential_parts[0]));
}
SIGNATURE => {
let base64_signature = Self::hex_to_base64(value)?;
context.set_signature(CheetahString::from(base64_signature));
}
_ => {
}
}
}
context.set_content(datetime.as_bytes().to_vec());
Ok(context)
}
fn build_from_remoting(
&self,
request: &RemotingCommand,
channel_id: Option<&str>,
) -> Result<DefaultAuthenticationContext, AuthError> {
let mut context = DefaultAuthenticationContext::new();
if let Some(id) = channel_id {
context.base.set_channel_id(Some(CheetahString::from(id)));
}
context
.base
.set_rpc_code(Some(CheetahString::from(request.code().to_string())));
let fields = match request.ext_fields() {
Some(fields) => fields,
None => return Ok(context), };
let access_key = match fields.get(&CheetahString::from(ACCESS_KEY)) {
Some(key) => key,
None => return Ok(context), };
context.set_username(access_key.clone());
if let Some(signature) = fields.get(&CheetahString::from(SIGNATURE_FIELD)) {
context.set_signature(signature.clone());
}
let mut sorted_map = BTreeMap::new();
for (key, value) in fields.iter() {
if request.version() <= RocketMqVersion::V4_9_3.ordinal() as i32 && key.as_str() == UNIQUE_MSG_QUERY_FLAG {
continue;
}
if key.as_str() != SIGNATURE_FIELD {
sorted_map.insert(key.clone(), value.clone());
}
}
let content = Self::combine_request_content(request, &sorted_map);
context.set_content(content);
Ok(context)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_to_base64() {
let hex = "48656C6C6F";
let result = DefaultAuthenticationContextBuilder::hex_to_base64(hex).unwrap();
assert_eq!(result, "SGVsbG8=");
}
#[test]
fn test_hex_to_base64_invalid() {
let hex = "ZZZZ";
let result = DefaultAuthenticationContextBuilder::hex_to_base64(hex);
assert!(result.is_err());
}
#[test]
fn test_build_from_remoting_no_ext_fields() {
use rocketmq_remoting::protocol::command_custom_header::CommandCustomHeader;
#[derive(Debug, Clone)]
struct EmptyHeader;
impl CommandCustomHeader for EmptyHeader {
fn to_map(
&self,
) -> Option<std::collections::HashMap<cheetah_string::CheetahString, cheetah_string::CheetahString>>
{
None
}
}
let builder = DefaultAuthenticationContextBuilder::new();
let request = RemotingCommand::create_request_command(1, EmptyHeader);
let result = builder.build_from_remoting(&request, Some("test-channel")).unwrap();
assert_eq!(result.base.channel_id(), Some(&CheetahString::from("test-channel")));
assert!(result.username().is_none());
}
}