feat(auth): add authentication protocol definitions and build configuration
- Add TokenClaims message for JWT payload structure with user id, issuer, timestamps, and scopes - Implement IssueTokenRequest/Response for creating access and refresh tokens with TTL support - Create RefreshTokenRequest/Response for token rotation functionality - Define RevokeTokenRequest/Response with support for single token or user-wide revocation - Add VerifyTokenRequest/Response for validating JWT tokens with detailed claims information - Implement signing key distribution system with GetSigningKeysRequest/Response - Create TokenService gRPC service with IssueToken, RefreshToken, RevokeToken, VerifyToken, and GetSigningKeys methods - Add build.rs configuration to compile proto files using tonic_prost_build - Include channel, channel_settings, member, and permission protocol definitions for IM services - Generate Rust code bindings through pb/core.rs and pb/im.rs modules
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::socket::packet::{Packet, PacketError, PacketType};
|
||||
|
||||
pub fn encode(packet: &Packet) -> String {
|
||||
let type_char = packet.packet_type as u8 + b'0';
|
||||
let mut result = String::new();
|
||||
|
||||
result.push(type_char as char);
|
||||
|
||||
if packet.has_binary() {
|
||||
result.push_str(&packet.attachment_count().to_string());
|
||||
result.push('-');
|
||||
}
|
||||
|
||||
if packet.namespace != "/" {
|
||||
result.push_str(&packet.namespace);
|
||||
result.push(',');
|
||||
}
|
||||
|
||||
if let Some(id) = packet.id {
|
||||
result.push_str(&id.to_string());
|
||||
}
|
||||
|
||||
if let Some(ref data) = packet.data {
|
||||
if packet.has_binary() {
|
||||
let data_with_placeholders = replace_binary_with_placeholders(data, packet.attachment_count());
|
||||
let encoded_data = serde_json::to_string(&data_with_placeholders)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!("Failed to serialize socket packet data: {}", e);
|
||||
"null".to_string()
|
||||
});
|
||||
result.push_str(&encoded_data);
|
||||
} else {
|
||||
let encoded_data = serde_json::to_string(data)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!("Failed to serialize socket packet data: {}", e);
|
||||
"null".to_string()
|
||||
});
|
||||
result.push_str(&encoded_data);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn encode_with_attachments(packet: &Packet) -> Vec<Vec<u8>> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
let encoded = encode(packet);
|
||||
result.push(encoded.into_bytes());
|
||||
|
||||
for attachment in &packet.attachments {
|
||||
result.push(attachment.clone());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn decode(input: &str) -> Result<Packet, PacketError> {
|
||||
if input.is_empty() {
|
||||
return Err(PacketError::Empty);
|
||||
}
|
||||
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
let type_char = chars.next().ok_or(PacketError::Empty)?;
|
||||
let packet_type = PacketType::try_from(type_char)?;
|
||||
|
||||
let attachment_count = if matches!(packet_type, PacketType::BinaryEvent | PacketType::BinaryAck) {
|
||||
let mut count_str = String::new();
|
||||
while let Some(&c) = chars.peek() {
|
||||
if c == '-' {
|
||||
chars.next();
|
||||
break;
|
||||
}
|
||||
if c.is_ascii_digit() {
|
||||
count_str.push(c);
|
||||
chars.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
count_str.parse::<usize>().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let remaining: String = chars.collect();
|
||||
|
||||
let (namespace, rest) = if let Some(after_slash) = remaining.strip_prefix('/') {
|
||||
// Check if this is a custom namespace (has a comma separating namespace from data/id)
|
||||
// or if '/' is just the root namespace prefix followed immediately by data
|
||||
if let Some(comma_pos) = after_slash.find(',') {
|
||||
let ns = format!("/{}", &after_slash[..comma_pos]);
|
||||
let rest = after_slash[comma_pos + 1..].to_string();
|
||||
(ns, rest)
|
||||
} else if after_slash.starts_with('[')
|
||||
|| after_slash.starts_with(|c: char| c.is_ascii_digit())
|
||||
|| after_slash.is_empty()
|
||||
{
|
||||
// '/[' means '/' is the root namespace and '[' starts the data
|
||||
// '/<digits>' means root namespace followed by ack id
|
||||
// '/' alone means disconnect on root namespace
|
||||
("/".to_string(), after_slash.to_string())
|
||||
} else {
|
||||
// Non-root namespace without data (e.g., disconnect on custom namespace)
|
||||
(remaining, String::new())
|
||||
}
|
||||
} else {
|
||||
("/".to_string(), remaining)
|
||||
};
|
||||
|
||||
let (id, data_str) = parse_id_and_data(&rest);
|
||||
|
||||
let data = if data_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(serde_json::from_str(&data_str)?)
|
||||
};
|
||||
|
||||
Ok(Packet {
|
||||
packet_type,
|
||||
namespace,
|
||||
data,
|
||||
id,
|
||||
attachments: Vec::new(),
|
||||
// Store attachment_count for binary packets; actual attachments come via decode_with_attachments
|
||||
expected_attachments: if attachment_count > 0 { Some(attachment_count) } else { None },
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_with_attachments(
|
||||
main_packet: &str,
|
||||
attachments: Vec<Vec<u8>>,
|
||||
) -> Result<Packet, PacketError> {
|
||||
let mut packet = decode(main_packet)?;
|
||||
|
||||
let expected = packet.expected_attachments.unwrap_or(0);
|
||||
if expected != attachments.len() {
|
||||
return Err(PacketError::InvalidAttachmentCount);
|
||||
}
|
||||
|
||||
packet.attachments = attachments;
|
||||
packet.expected_attachments = None;
|
||||
|
||||
if packet.has_binary() {
|
||||
if let Some(ref data) = packet.data {
|
||||
packet.data = Some(replace_placeholders_with_binary(data, &packet.attachments));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
fn parse_id_and_data(input: &str) -> (Option<u64>, String) {
|
||||
let mut id_str = String::new();
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
while let Some(&c) = chars.peek() {
|
||||
if c.is_ascii_digit() {
|
||||
id_str.push(c);
|
||||
chars.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let id = if id_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
id_str.parse::<u64>().ok()
|
||||
};
|
||||
|
||||
let data: String = chars.collect();
|
||||
|
||||
(id, data)
|
||||
}
|
||||
|
||||
/// Replace binary values in the data with { "_placeholder": true, "num": N } placeholders.
|
||||
/// This is used when encoding binary events/acks for transmission over text-based transports.
|
||||
fn replace_binary_with_placeholders(value: &Value, total_attachments: usize) -> Value {
|
||||
match value {
|
||||
Value::Array(arr) => {
|
||||
let mut placeholder_idx = total_attachments; // Start from known count
|
||||
let new_arr: Vec<Value> = arr
|
||||
.iter()
|
||||
.map(|v| replace_binary_with_placeholders_inner(v, &mut placeholder_idx))
|
||||
.collect();
|
||||
Value::Array(new_arr)
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let mut placeholder_idx = total_attachments;
|
||||
let mut new_map = serde_json::Map::new();
|
||||
for (k, v) in map {
|
||||
new_map.insert(
|
||||
k.clone(),
|
||||
replace_binary_with_placeholders_inner(v, &mut placeholder_idx),
|
||||
);
|
||||
}
|
||||
Value::Object(new_map)
|
||||
}
|
||||
_ => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_binary_with_placeholders_inner(value: &Value, placeholder_idx: &mut usize) -> Value {
|
||||
match value {
|
||||
Value::Array(arr) => {
|
||||
let new_arr: Vec<Value> = arr
|
||||
.iter()
|
||||
.map(|v| replace_binary_with_placeholders_inner(v, placeholder_idx))
|
||||
.collect();
|
||||
Value::Array(new_arr)
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let mut new_map = serde_json::Map::new();
|
||||
for (k, v) in map {
|
||||
new_map.insert(
|
||||
k.clone(),
|
||||
replace_binary_with_placeholders_inner(v, placeholder_idx),
|
||||
);
|
||||
}
|
||||
Value::Object(new_map)
|
||||
}
|
||||
// Binary data would be represented as base64 strings in the initial data;
|
||||
// in the Socket.IO protocol, binary attachments are separate and referenced by placeholder.
|
||||
// This function handles the case where the data structure itself contains placeholder markers.
|
||||
_ => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_placeholders_with_binary(value: &Value, attachments: &[Vec<u8>]) -> Value {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
// Check if this is a placeholder object: { "_placeholder": true, "num": N }
|
||||
if let (Some(Value::Bool(true)), Some(Value::Number(num))) =
|
||||
(map.get("_placeholder"), map.get("num"))
|
||||
{
|
||||
if let Some(idx) = num.as_u64() {
|
||||
if let Some(attachment) = attachments.get(idx as usize) {
|
||||
return Value::String(base64::Engine::encode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
attachment,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_map = serde_json::Map::new();
|
||||
for (k, v) in map {
|
||||
new_map.insert(k.clone(), replace_placeholders_with_binary(v, attachments));
|
||||
}
|
||||
Value::Object(new_map)
|
||||
}
|
||||
Value::Array(arr) => Value::Array(
|
||||
arr.iter()
|
||||
.map(|v| replace_placeholders_with_binary(v, attachments))
|
||||
.collect(),
|
||||
),
|
||||
_ => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_encode_connect() {
|
||||
let packet = Packet::connect("/", None);
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "0");
|
||||
|
||||
let packet = Packet::connect("/admin", Some(json!({"sid": "abc"})));
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "0/admin,{\"sid\":\"abc\"}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_event() {
|
||||
let packet = Packet::event("/", json!(["foo"]), None);
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "2[\"foo\"]");
|
||||
|
||||
let packet = Packet::event("/admin", json!(["bar"]), None);
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "2/admin,[\"bar\"]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_event_with_ack() {
|
||||
let packet = Packet::event("/", json!(["foo"]), Some(12));
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "212[\"foo\"]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_ack() {
|
||||
let packet = Packet::ack("/", json!([]), 12);
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "312[]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_disconnect() {
|
||||
let packet = Packet::disconnect("/");
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "1");
|
||||
|
||||
let packet = Packet::disconnect("/admin");
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "1/admin,");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_connect_error() {
|
||||
let packet = Packet::connect_error("/", "Not authorized");
|
||||
let encoded = encode(&packet);
|
||||
assert_eq!(encoded, "4{\"message\":\"Not authorized\"}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_connect() {
|
||||
let packet = decode("0").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Connect);
|
||||
assert_eq!(packet.namespace, "/");
|
||||
assert!(packet.data.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_connect_with_namespace() {
|
||||
let packet = decode("0/admin,{\"sid\":\"abc\"}").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Connect);
|
||||
assert_eq!(packet.namespace, "/admin");
|
||||
assert!(packet.data.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_event() {
|
||||
let packet = decode("2[\"foo\"]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Event);
|
||||
assert_eq!(packet.namespace, "/");
|
||||
assert_eq!(packet.data, Some(json!(["foo"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_event_with_namespace() {
|
||||
let packet = decode("2/admin,[\"bar\"]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Event);
|
||||
assert_eq!(packet.namespace, "/admin");
|
||||
assert_eq!(packet.data, Some(json!(["bar"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_event_with_ack() {
|
||||
let packet = decode("212[\"foo\"]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Event);
|
||||
assert_eq!(packet.id, Some(12));
|
||||
assert_eq!(packet.data, Some(json!(["foo"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_ack() {
|
||||
let packet = decode("312[]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Ack);
|
||||
assert_eq!(packet.id, Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_disconnect() {
|
||||
let packet = decode("1").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Disconnect);
|
||||
assert_eq!(packet.namespace, "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_disconnect_with_namespace() {
|
||||
let packet = decode("1/admin,").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::Disconnect);
|
||||
assert_eq!(packet.namespace, "/admin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_binary_event_attachment_count() {
|
||||
let packet = decode("51-[\"baz\",{\"_placeholder\":true,\"num\":0}]").unwrap();
|
||||
assert_eq!(packet.packet_type, PacketType::BinaryEvent);
|
||||
assert_eq!(packet.expected_attachments, Some(1));
|
||||
assert_eq!(packet.namespace, "/");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user