From 5eb60dbbc6a1b5319eb68ae15c9a1db8b1828058 Mon Sep 17 00:00:00 2001 From: Eigeen Date: Sun, 7 Jan 2024 23:09:04 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 ++ Cargo.toml | 16 +++ LICENSE | 13 ++ src/core/liveroom.rs | 0 src/core/message.rs | 76 ++++++++++ src/core/mod.rs | 3 + src/core/packet.rs | 321 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 6 + src/utils/brotli.rs | 9 ++ src/utils/macros.rs | 6 + src/utils/mod.rs | 2 + 11 files changed, 462 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 src/core/liveroom.rs create mode 100644 src/core/message.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/packet.rs create mode 100644 src/main.rs create mode 100644 src/utils/brotli.rs create mode 100644 src/utils/macros.rs create mode 100644 src/utils/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..088ba6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..84d9708 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "bili-live-danmaku-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bincode = "1.3.3" +brotli = "3.4.0" +futures = "0.3.30" +serde = { version = "1.0.195", features = ["derive"] } +serde_bytes = "0.11.14" +serde_json = "1.0.111" +strum_macros = "0.25.3" +thiserror = "1.0.56" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ac2393 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2024 Eigeen + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. \ No newline at end of file diff --git a/src/core/liveroom.rs b/src/core/liveroom.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/core/message.rs b/src/core/message.rs new file mode 100644 index 0000000..6038fdc --- /dev/null +++ b/src/core/message.rs @@ -0,0 +1,76 @@ +use std::io; +use serde::Serialize; +use strum_macros::{AsRefStr, Display, EnumString}; +use thiserror::Error; +use crate::core::packet::{Packet, PacketError}; + +// #[allow(unused_variables, dead_code)] +#[derive(Error, Debug)] +pub enum MessageError { + #[error("{0}")] + InvalidPacket(#[from] PacketError), + #[error("no message available")] + NoMessage, + #[error("unknown message error")] + Unknown, +} + +/// 命令名对照表,用于对应解析命令名称 +// #[allow(unused_variables, dead_code)] +#[derive(Debug, PartialEq, Display, AsRefStr, EnumString)] +pub enum Command { + Undefined, + #[strum(serialize = "DANMU_MSG")] + DanmuMsg, + #[strum(serialize = "SUPER_CHAT_MESSAGE")] + SuperChatMessage, + #[strum(serialize = "SUPER_CHAT_MESSAGE_DELETE")] + SuperChatMessageDelete, + #[strum(serialize = "SEND_GIFT")] + SendGift, +} + +impl Default for Command { + fn default() -> Self { + Self::Undefined + } +} + +impl Command { + pub fn from_str(cmd: &str) -> Self { + cmd.parse().unwrap_or_default() + } +} + +#[allow(unused_variables, dead_code)] +#[derive(Serialize, Debug)] +pub enum Message { + DanmuMsg {}, + SuperChatMessage, + SuperChatMessageDelete, +} + +impl Message { + fn deserialize_json() {} + + fn parse_packet_skip_err(single_packet: &Packet) -> Message {} + + /// 从 Packet 中提取 Message + /// + /// 只有 Json 格式的非业务消息会被提取,即心跳包回复,认证请求回复等不会被解析。 + pub fn from_packet(packet: &mut Packet) -> Result, MessageError> { + let mut packets = packet.get_body_packets()?; + if packets.len() == 0 { + return Err(MessageError::NoMessage); + }; + + let messages = packets.iter().map(|packet| { + Self::parse_packet_skip_err(packet) + }).collect(); + if messages.len() == 0 { + Err(MessageError::NoMessage) + } else { + Ok(messages) + } + } +} \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..e118529 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,3 @@ +pub mod message; +pub mod liveroom; +pub mod packet; diff --git a/src/core/packet.rs b/src/core/packet.rs new file mode 100644 index 0000000..51ec131 --- /dev/null +++ b/src/core/packet.rs @@ -0,0 +1,321 @@ +use std::io; + +use bincode::Options; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::{str_to_u8_array, utils}; + +// #[allow(unused_variables, dead_code)] +#[derive(Error, Debug)] +pub enum PacketError { + #[error("deserialize packet header error")] + InvalidHeader, + #[error("operation code {0} is unsupported")] + UnsupportedOpCode(u32), + #[error("protocol version {0} is unsupported")] + UnsupportedProtoVer(u16), + #[error("{0}")] + DecompressError(#[from] io::Error), + #[error("parse packet error")] + UnpackError, + #[error("unknown packet error")] + Unknown, +} + +/// 操作码 +#[allow(unused_variables)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum OperationCode { + Unknown = 0, + Heartbeat = 2, + HeartbeatResponse = 3, + Message = 5, + Auth = 7, + AuthResponse = 8, +} + +/// 协议版本 +/// +/// 0 业务通信消息,无压缩 \ +/// 1 连接通信消息,无压缩 (比如心跳包、认证包等与业务无关的数据包) \ +/// 2 无压缩消息 \ +/// 3 Brotli 压缩 +#[allow(unused_variables)] +pub enum ProtocolVersion { + Unknown = -1, + Business = 0, + Connection = 1, + Normal = 2, + Brotli = 3, +} + +/// 数据包头 +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +struct PacketHeader { + total_size: u32, + header_size: u16, + protocol_version: u16, + operation_code: u32, + sequence: u32, +} + +const PACKET_HEADER_SIZE: usize = std::mem::size_of::(); + +impl PacketHeader { + fn new(payload_size: usize, operation_code: OperationCode) -> Self { + PacketHeader { + total_size: payload_size as u32 + PACKET_HEADER_SIZE as u32, + header_size: PACKET_HEADER_SIZE as u16, + protocol_version: 0, + operation_code: operation_code as u32, + sequence: 0, + } + } + + fn from_bytes(bytes: &[u8]) -> Result { + let result = bincode::DefaultOptions::new() + .with_big_endian() + .with_fixint_encoding() + .allow_trailing_bytes() + .deserialize(&bytes[..PACKET_HEADER_SIZE]); + match result { + Ok(res) => Ok(res), + Err(_) => Err(PacketError::InvalidHeader), + } + } + + #[allow(dead_code)] + fn operation_code(&self) -> OperationCode { + match self.operation_code { + 2 => OperationCode::Heartbeat, + 3 => OperationCode::HeartbeatResponse, + 5 => OperationCode::Message, + 7 => OperationCode::Auth, + 8 => OperationCode::AuthResponse, + _ => OperationCode::Unknown, + } + } + + fn protocol_version(&self) -> ProtocolVersion { + match self.protocol_version { + 0 => ProtocolVersion::Business, + 1 => ProtocolVersion::Connection, + 2 => ProtocolVersion::Normal, + 3 => ProtocolVersion::Brotli, + _ => ProtocolVersion::Unknown, + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthPayload { + uid: u64, + #[serde(rename = "roomid")] + room_id: u64, + #[serde(rename = "protover")] + proto_ver: u8, + buvid: String, + platform: String, + #[serde(rename = "type")] + auth_type: u8, + key: String, +} + +impl Default for AuthPayload { + fn default() -> Self { + Self { + uid: 0, + room_id: 0, + proto_ver: 3, + buvid: "".to_owned(), + platform: "web".to_owned(), + auth_type: 2, + key: "".to_owned(), + } + } +} + +/// 数据包 +/// +/// payload 通常是 json 格式 +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Packet { + header: PacketHeader, + payload: Vec, + parsed_packets: Option>, +} + +impl Packet { + pub fn new_auth(auth: &AuthPayload) -> Self { + // { + // "uid": 90931399, + // "roomid": 15028238, + // "protover": 3, + // "buvid": "FB7F3F89-C9DA-C58B-25F4-2EECA80D6A6F73048infoc", + // "platform": "web", + // "type": 2, + // "key": "hm7gVA1C1f-4NNM9IGhpT_ucAF7L3OcO7QVNdNaTKwFRwIUv1Dc1HJLJI3G24TMJEZ9r_eUpbl78gOaNjCHuyDsU1eZyQA_4vPlQj5o0X6cQNScrrh9134Uxwnc5qvoirspInBWyrioK3LKzNXG4Mg3sIcvewEslBz6OWNTQVJvXCm8Y3rXdTepPXQ==" + // } + + // 省略参数校验 + let payload = serde_json::to_string(&auth).unwrap() + .into_bytes(); + Self { + header: PacketHeader::new(payload.len(), OperationCode::Auth), + payload, + parsed_packets: None, + } + } + + pub fn new_heartbeat() -> Self { + Self { + header: PacketHeader::new(31, OperationCode::Heartbeat), + payload: "[object Object]".as_bytes().to_vec(), + parsed_packets: None, + } + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let header = PacketHeader::from_bytes(&bytes[..PACKET_HEADER_SIZE])?; + // 由于 brotli header长度是记录的第一个分包的长度,因此需要将后面所有字节都写入 + let payload_slice = &bytes[PACKET_HEADER_SIZE..]; + Ok(Self { + header, + payload: payload_slice.to_vec(), + parsed_packets: None, + }) + } + + // #[inline] + // pub fn get_operation_code(&self) -> OperationCode { + // self.header.operation_code() + // } + + fn decompress_brotli(input: &[u8]) -> Result, PacketError> { + let result = utils::brotli::decompress_to_vec(input); + match result { + Ok(res) => Ok(res), + Err(err) => Err(PacketError::DecompressError(err)) + } + } + + /// 解析 Brotli 压缩格式的封包 + /// + /// 递归解析,分块解析为多个Normal消息 + fn parse_body_brotli(&self) -> Result, PacketError> { + let dec_bytes = Self::decompress_brotli(&*self.payload)?; + // 分块解析为多个Normal消息 + let mut packets: Vec = Vec::new(); + let mut offset = 0; + while offset < dec_bytes.len() { + let header = PacketHeader::from_bytes(&dec_bytes[offset..offset + PACKET_HEADER_SIZE])?; + let block_bytes = &dec_bytes[offset..offset + header.total_size as usize]; + packets.push(Packet::from_bytes(block_bytes)?); + offset += header.total_size as usize; + } + + Ok(packets) + } + + pub fn parse_body(&mut self) -> Result<(), PacketError> { + match self.header.protocol_version() { + ProtocolVersion::Brotli => { + self.parsed_packets = Some(self.parse_body_brotli()?); + Ok(()) + } + // 未加密消息:在 parsed_packets 中复制一份自身 此处可优化 + ProtocolVersion::Business | + ProtocolVersion::Connection | + ProtocolVersion::Normal => { + self.parsed_packets = Some(vec![self.clone()]); + Ok(()) + } + _ => Err(PacketError::UnsupportedProtoVer(self.header.protocol_version)) + } + } + + pub fn get_body(&mut self) -> Result>, PacketError> { + if self.parsed_packets.is_none() { + self.parse_body()?; + } + + let payloads = self.parsed_packets.as_ref().unwrap().iter().map(|packet| { + packet.payload.clone() + }).collect(); + Ok(payloads) + } + + pub fn get_body_packets(&mut self) -> Result<&Vec, PacketError> { + if self.parsed_packets.is_none() { + self.parse_body()?; + } + + Ok(self.parsed_packets.as_ref().unwrap()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_heartbeat_resp() { + // [object Object] + let expect = vec![vec![0, 0, 0, 1, 91, 111, 98, 106, 101, 99, 116, 32, 79, 98, 106, 101, 99, 116, 93]]; + let buffer = [0, 0, 0, 20, 0, 16, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 91, 111, 98, 106, 101, 99, 116, 32, 79, 98, 106, 101, 99, 116, 93]; + let mut packet = Packet::from_bytes(&buffer).unwrap(); + let body = packet.get_body().unwrap(); + assert_eq!(packet.header.operation_code(), OperationCode::HeartbeatResponse); + assert_eq!(body, expect); + } + + #[test] + fn test_new_heartbeat() { + // [object Object] + let expect = vec![vec![91, 111, 98, 106, 101, 99, 116, 32, 79, 98, 106, 101, 99, 116, 93]]; + let mut packet = Packet::new_heartbeat(); + let body = packet.get_body().unwrap(); + assert_eq!(packet.header.operation_code(), OperationCode::Heartbeat); + assert_eq!(body, expect); + } + + #[test] + fn test_auth_ok() { + let expect = vec![str_to_u8_array!("{\"code\":0}").to_vec()]; + let buffer = [0, 0, 0, 26, 0, 16, 0, 1, 0, 0, 0, 8, 0, 0, 0, 1, 123, 34, 99, 111, 100, 101, 34, 58, 48, 125]; + let mut packet = Packet::from_bytes(&buffer).unwrap(); + let body = packet.get_body().unwrap(); + assert_eq!(packet.header.operation_code(), OperationCode::AuthResponse); + assert_eq!(body, expect); + } + + #[test] + fn test_new_auth() { + let mut packet = Packet::new_auth(&AuthPayload { + uid: 999, + room_id: 999, + buvid: "ABC".to_string(), + key: "DEF".to_string(), + ..AuthPayload::default() + }); + assert_eq!(packet.header.operation_code(), OperationCode::Auth); + } + + #[test] + fn test_brotli_1() { + let expect = str_to_u8_array!("{\"cmd\":\"INTERACT_WORD\",\"data\":{\"contribution\":"); + let buffer = [0, 0, 3, 5, 0, 16, 0, 3, 0, 0, 0, 5, 0, 0, 0, 0, 27, 130, 13, 0, 60, 20, 111, 44, 38, 213, 160, 39, 87, 39, 26, 178, 131, 23, 216, 96, 168, 81, 191, 142, 184, 5, 82, 42, 187, 193, 243, 128, 227, 68, 130, 32, 200, 138, 131, 210, 240, 20, 72, 68, 177, 253, 149, 206, 45, 228, 76, 162, 192, 216, 32, 208, 217, 44, 58, 229, 201, 89, 180, 220, 150, 250, 133, 2, 200, 160, 68, 67, 189, 129, 255, 234, 111, 64, 224, 13, 49, 107, 234, 42, 17, 15, 142, 78, 99, 123, 252, 33, 92, 21, 36, 187, 89, 181, 160, 180, 228, 116, 193, 18, 58, 37, 18, 83, 0, 125, 53, 36, 101, 245, 251, 105, 228, 55, 50, 202, 197, 147, 242, 153, 8, 151, 219, 255, 47, 64, 32, 36, 97, 225, 189, 82, 80, 87, 151, 29, 218, 178, 35, 163, 42, 117, 133, 172, 241, 79, 199, 85, 184, 78, 93, 85, 134, 115, 42, 23, 240, 20, 114, 147, 70, 128, 14, 44, 6, 191, 153, 95, 95, 250, 20, 140, 116, 150, 56, 40, 237, 63, 196, 141, 15, 43, 71, 225, 141, 135, 37, 72, 13, 248, 93, 4, 62, 218, 135, 248, 127, 140, 241, 9, 103, 63, 97, 110, 192, 112, 126, 132, 133, 206, 193, 106, 126, 43, 35, 4, 35, 176, 144, 24, 51, 97, 52, 64, 106, 18, 109, 3, 255, 84, 218, 112, 85, 23, 162, 159, 224, 126, 96, 2, 113, 104, 169, 213, 244, 43, 5, 3, 140, 36, 220, 33, 108, 139, 252, 149, 243, 9, 103, 116, 125, 58, 106, 2, 7, 3, 4, 207, 61, 95, 153, 234, 46, 174, 19, 36, 114, 50, 6, 178, 103, 201, 224, 138, 83, 168, 13, 128, 189, 173, 77, 14, 87, 90, 169, 72, 82, 251, 8, 67, 118, 106, 235, 127, 223, 141, 56, 55, 56, 32, 130, 40, 25, 121, 75, 71, 76, 23, 33, 166, 23, 141, 194, 22, 183, 252, 12, 169, 50, 60, 200, 144, 97, 47, 35, 121, 193, 212, 241, 149, 231, 111, 0, 12, 70, 49, 33, 149, 66, 89, 79, 223, 64, 168, 52, 253, 129, 95, 147, 77, 175, 253, 217, 36, 17, 1, 222, 44, 161, 119, 210, 25, 14, 23, 42, 115, 114, 214, 79, 190, 64, 208, 89, 145, 146, 59, 199, 37, 111, 131, 194, 193, 0, 21, 78, 12, 25, 63, 56, 139, 28, 215, 208, 174, 205, 140, 41, 185, 251, 105, 100, 179, 110, 163, 11, 59, 37, 184, 51, 129, 193, 25, 168, 240, 104, 13, 170, 193, 215, 248, 27, 89, 132, 78, 83, 6, 52, 46, 157, 136, 254, 135, 44, 189, 94, 241, 209, 51, 110, 125, 223, 30, 204, 124, 94, 136, 3, 122, 220, 120, 64, 59, 187, 37, 189, 119, 9, 226, 212, 238, 35, 151, 157, 71, 180, 149, 115, 34, 1, 139, 161, 101, 217, 62, 2, 83, 246, 43, 43, 243, 97, 112, 104, 113, 138, 43, 98, 34, 10, 32, 163, 98, 248, 196, 129, 161, 236, 70, 28, 251, 223, 120, 125, 123, 132, 96, 106, 89, 145, 162, 12, 106, 7, 184, 245, 13, 251, 127, 20, 80, 101, 170, 69, 180, 89, 18, 24, 202, 26, 230, 0, 55, 205, 2, 43, 73, 240, 209, 52, 48, 209, 52, 52, 155, 248, 103, 78, 16, 207, 162, 32, 28, 165, 59, 102, 133, 159, 101, 93, 58, 49, 17, 128, 119, 234, 32, 161, 244, 198, 102, 252, 182, 213, 210, 171, 109, 53, 105, 30, 102, 182, 141, 236, 181, 175, 28, 221, 241, 220, 121, 85, 229, 109, 117, 23, 119, 16, 252, 71, 215, 143, 32, 26, 245, 130, 38, 8, 135, 152, 99, 212, 247, 124, 90, 217, 144, 218, 118, 6, 101, 136, 101, 77, 237, 194, 145, 240, 251, 77, 163, 21, 145, 93, 110, 167, 53, 149, 21, 215, 125, 170, 26, 166, 227, 244, 232, 164, 180, 49, 111, 191, 242, 60, 173, 171, 75, 212, 166, 156, 26, 142, 89, 89, 205, 64, 57, 133, 29, 172, 11, 18, 209, 239, 75, 130, 133, 125, 105, 17, 90, 230, 214, 122, 23, 146, 21, 153, 217, 209, 202, 93, 176, 63, 158, 231, 84, 187, 118, 175, 43, 129, 205, 92, 37, 242, 149, 21, 94, 104, 123, 90, 15, 86, 220, 174, 167, 119, 125, 23, 15, 247, 141, 22, 162, 90, 185, 160, 137, 194, 33, 174, 212, 247, 124, 106, 225, 144, 226, 182, 70, 133, 92, 4]; + let body = Packet::from_bytes(&buffer).unwrap().get_body().unwrap(); + assert_eq!(body.len(), 3); // 主包长度检查 + assert_eq!(&body[0][..46], expect); // 子包0内容检查 + } + + #[test] + fn test_uncommon() { + let expect = vec![str_to_u8_array!(r#"{"cmd":"ONLINE_RANK_COUNT","data":{"count":419}}"#).to_vec()]; + let buffer = [0, 0, 0, 84, 0, 16, 0, 3, 0, 0, 0, 5, 0, 0, 0, 0, 139, 31, 128, 0, 0, 0, 64, 0, 16, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 123, 34, 99, 109, 100, 34, 58, 34, 79, 78, 76, 73, 78, 69, 95, 82, 65, 78, 75, 95, 67, 79, 85, 78, 84, 34, 44, 34, 100, 97, 116, 97, 34, 58, 123, 34, 99, 111, 117, 110, 116, 34, 58, 52, 49, 57, 125, 125, 3]; + let body = Packet::from_bytes(&buffer).unwrap().get_body().unwrap(); + assert_eq!(body, expect); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0abe601 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +mod core; +mod utils; + +fn main() { + println!("Hello, world!"); +} diff --git a/src/utils/brotli.rs b/src/utils/brotli.rs new file mode 100644 index 0000000..38a9cc3 --- /dev/null +++ b/src/utils/brotli.rs @@ -0,0 +1,9 @@ +use std::io::{Error, Read}; +use brotli::Decompressor; + +pub fn decompress_to_vec(input: &[u8]) -> Result, Error> { + let mut reader = Decompressor::new(input, 4096); + let mut result: Vec = Vec::new(); + reader.read_to_end(&mut result)?; + Ok(result) +} diff --git a/src/utils/macros.rs b/src/utils/macros.rs new file mode 100644 index 0000000..3366993 --- /dev/null +++ b/src/utils/macros.rs @@ -0,0 +1,6 @@ +#[macro_export] +macro_rules! str_to_u8_array { + ($str:expr) => { + $str.as_bytes() + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..0de5fcc --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod brotli; +pub mod macros; \ No newline at end of file