crowdstrike_cloudproto/services/lfo/
request.rs

1use crate::services::lfo::CompressionFormats;
2use crate::services::{DEFAULT_AID_HEX, DEFAULT_CID_HEX};
3
4/// Ask for a single file on a remote LFO server by path.
5///
6/// By default requests indicate support for XZ compression, but this is configurable.
7/// Even if a request accepts compression, the server may decide to reply with an uncompressed
8/// response if the requested file is itself an archive on disk.
9///
10/// Requests contain the CID (Customer ID) and AID (Agent ID) of the client, but the LFO server
11/// will accept any value for these, so in practice no authentication is required.
12#[derive(Eq, PartialEq, Debug, Clone)]
13pub struct LfoRequest {
14    // The CID assigned to a Crowdstrike customer (same as the CCID without the last -N number)
15    // The LFO server doesn't really check if it belongs to anyone. Just try to pass a valid CID.
16    pub(crate) cid: [u8; 16],
17    // Agent ID. LFO isn't uptight like TS if the AID is not an active customer.
18    // In fact, you can give it all zeroes. LFO is friendly like that.
19    pub(crate) aid: [u8; 16],
20    // The real client supports values 0 or 1. We only support 0.
21    pub(crate) compression: u16,
22    // The file to download
23    pub(crate) remote_path: String,
24    // This field is probably the offset for chunked downloads. Not supported or tested yet.
25    // Large files can't be downloaded in one packet, so the client may get partial responses
26    // The offset allows downloading the rest of those large files in multiple queries
27    pub(crate) offset: u32,
28}
29
30impl LfoRequest {
31    /// Create a request for `remote_path` with default values
32    pub fn new_simple(remote_path: String) -> Self {
33        Self {
34            // LFO doesn't mind all zeroes
35            cid: hex::decode(DEFAULT_CID_HEX).unwrap().try_into().unwrap(),
36            aid: hex::decode(DEFAULT_AID_HEX).unwrap().try_into().unwrap(),
37            compression: 0,
38            remote_path,
39            offset: 0,
40        }
41    }
42
43    pub fn new_custom(
44        cid: [u8; 16],
45        aid: [u8; 16],
46        compression: CompressionFormats,
47        remote_path: String,
48    ) -> Self {
49        Self {
50            cid,
51            aid,
52            compression: compression as u16,
53            remote_path,
54            // Only 0 if supported for now
55            // The receive side WILL break right now if it sees a reply with non-zero offset
56            offset: 0,
57        }
58    }
59
60    pub(crate) fn to_payload(&self) -> Vec<u8> {
61        let mut payload = vec![];
62        payload.extend_from_slice(&self.cid); // CU "simple store" value
63        payload.extend_from_slice(&self.aid); // AG "simple store" value
64        payload.extend_from_slice(8u32.to_be_bytes().as_slice());
65        payload.extend_from_slice(&self.offset.to_be_bytes());
66        payload.extend_from_slice(&self.compression.to_be_bytes());
67        payload.extend_from_slice(self.remote_path.as_bytes());
68        payload
69    }
70
71    #[cfg(test)]
72    pub(crate) fn try_from_payload(payload: &[u8]) -> Result<Self, super::LfoError> {
73        use super::LfoError;
74        use byteorder::{ReadBytesExt, BE};
75        use std::io::Read;
76
77        let mut cursor = std::io::Cursor::new(payload);
78        let mut cid = [0u8; 16];
79        cursor.read_exact(&mut cid)?;
80        let mut aid = [0u8; 16];
81        cursor.read_exact(&mut aid)?;
82        _ = cursor.read_u32::<BE>()?;
83        let offset = cursor.read_u32::<BE>()?;
84        let compression = cursor.read_u16::<BE>()?;
85        let remote_path = String::from_utf8(payload[cursor.position() as usize..].into())
86            .map_err(|_| LfoError::InvalidRequest)?;
87        Ok(Self {
88            cid,
89            aid,
90            compression,
91            remote_path,
92            offset,
93        })
94    }
95}