Skip to main content

futu_backend/
session_key_refresh.rs

1use futu_command_spec::{CommandSpecId, command_spec};
2use futu_core::error::{FutuError, Result};
3use futu_domain_auth::{
4    SessionKeyRefreshChannelKind, SessionKeyRefreshPlan, SessionKeyRefreshReplyFacts,
5    SessionKeyRefreshWireKind, ensure_session_key_refresh_success_like_cpp,
6    plan_session_key_refresh_like_cpp,
7};
8use futu_proto_internal::f3clogin_ft_conn_session_key::{
9    RefreshSessionKeyReq, RefreshSessionKeyRsp,
10};
11use prost::Message;
12
13use crate::conn::BackendConn;
14use crate::connection_lifecycle_runtime::execute_connection_lifecycle;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct SessionKeyRefreshOutcome {
18    pub command_id: u16,
19    pub new_session_key_len: usize,
20}
21
22/// Send one C++-aligned session-key refresh transaction and install the key.
23///
24/// Timer ownership and reconnect effects belong to the gateway connection
25/// lifecycle actor. This adapter owns only request encoding, backend I/O,
26/// response decoding, and the atomic `BackendConn` key swap.
27pub async fn refresh_session_key(
28    conn: &BackendConn,
29    channel: SessionKeyRefreshChannelKind,
30    client_key_len: usize,
31) -> Result<SessionKeyRefreshOutcome> {
32    let plan = plan_session_key_refresh_like_cpp(channel, client_key_len);
33    let spec = command_spec(plan.command_id).ok_or_else(|| {
34        FutuError::Codec(format!(
35            "session-key refresh command {} is not registered",
36            plan.command_id
37        ))
38    })?;
39    if !matches!(spec.id, CommandSpecId::SessionKeyRefresh(_)) {
40        return Err(FutuError::Codec(format!(
41            "command {} is not a session-key refresh identity",
42            plan.command_id
43        )));
44    }
45
46    let request_body = encode_request(plan);
47    let response = execute_connection_lifecycle(conn, spec.id, request_body).await?;
48    let facts = decode_response(plan, response.body.as_ref())?;
49    let new_session_key = ensure_session_key_refresh_success_like_cpp(facts)
50        .map_err(|error| FutuError::Codec(error.to_string()))?;
51    let new_session_key_len = new_session_key.len();
52    conn.set_session_key(new_session_key);
53
54    Ok(SessionKeyRefreshOutcome {
55        command_id: spec.cmd_id,
56        new_session_key_len,
57    })
58}
59
60fn encode_request(plan: SessionKeyRefreshPlan) -> Vec<u8> {
61    match plan.wire {
62        SessionKeyRefreshWireKind::LegacyBinary => Vec::new(),
63        SessionKeyRefreshWireKind::Protobuf => RefreshSessionKeyReq {}.encode_to_vec(),
64    }
65}
66
67fn decode_response(
68    plan: SessionKeyRefreshPlan,
69    body: &[u8],
70) -> Result<SessionKeyRefreshReplyFacts> {
71    match plan.wire {
72        SessionKeyRefreshWireKind::LegacyBinary => decode_legacy_response(body),
73        SessionKeyRefreshWireKind::Protobuf => {
74            let response = RefreshSessionKeyRsp::decode(body).map_err(|error| {
75                FutuError::Codec(format!("decode session-key refresh PB: {error}"))
76            })?;
77            Ok(SessionKeyRefreshReplyFacts {
78                result_code: response.result_code,
79                new_session_key: response.new_session_key,
80            })
81        }
82    }
83}
84
85fn decode_legacy_response(body: &[u8]) -> Result<SessionKeyRefreshReplyFacts> {
86    let Some(&result_code) = body.first() else {
87        return Err(FutuError::Codec(
88            "session-key refresh legacy response is empty".into(),
89        ));
90    };
91    let result_code = i8::from_ne_bytes([result_code]) as i32;
92    let new_session_key = if result_code == 0 {
93        let mut key = vec![0_u8; 16];
94        // C++ initializes the 16-byte destination to zero and does not inspect
95        // `OMBinDeSrz::IsOK()` after the fixed-buffer read. A truncated success
96        // body therefore installs the all-zero initialized buffer rather than
97        // producing a second parser error.
98        // Ref: `logger.cpp:1782-1806` and `OMBinSrzOperate.hpp:112-169`.
99        if body.len() >= 17 {
100            key.copy_from_slice(&body[1..17]);
101        }
102        Some(key)
103    } else {
104        None
105    };
106    Ok(SessionKeyRefreshReplyFacts {
107        result_code: Some(result_code),
108        new_session_key,
109    })
110}
111
112#[cfg(test)]
113mod tests;