1use std::time::Duration;
2
3use crate::config::CortexConfig;
4use crate::error::{SdkError, SdkResult};
5use crate::models::*;
6
7pub struct CortexClient {
12 config: CortexConfig,
13 http: reqwest::Client,
14 heartbeat_handle: Option<tokio::task::JoinHandle<()>>,
15}
16
17impl CortexClient {
18 pub fn new(config: CortexConfig) -> Self {
19 let http = reqwest::Client::builder()
20 .timeout(Duration::from_secs(config.timeout))
21 .build()
22 .expect("Failed to create HTTP client");
23 let mut client = Self {
24 config,
25 http,
26 heartbeat_handle: None,
27 };
28 client.start_heartbeat();
29 client
30 }
31
32 fn bearer(&self) -> String {
33 format!("Bearer {}", self.config.pat)
34 }
35
36 fn auth_get(&self, url: &str) -> reqwest::RequestBuilder {
37 self.http.get(url).header("Authorization", self.bearer())
38 }
39
40 fn auth_post(&self, url: &str, body: &impl serde::Serialize) -> reqwest::RequestBuilder {
41 self.http
42 .post(url)
43 .header("Authorization", self.bearer())
44 .json(body)
45 }
46
47 async fn decode<T: serde::de::DeserializeOwned>(&self, resp: reqwest::Response) -> SdkResult<T> {
48 let api_resp: ApiResponse<T> = resp.json().await?;
49 if !api_resp.success {
50 return Err(SdkError::ServerError(api_resp.message.unwrap_or_default()));
51 }
52 api_resp
53 .data
54 .ok_or_else(|| SdkError::ServerError("No data".to_string()))
55 }
56
57 pub async fn list_software(&self) -> SdkResult<Vec<SoftwareStoreItem>> {
60 let url = format!("{}/api/softwares/store", self.config.server_url);
61 let resp = self.auth_get(&url).send().await?;
62 self.decode(resp).await
63 }
64
65 pub async fn get_latest_version(&self, software_id: &str) -> SdkResult<SoftwareVersion> {
66 let url = format!(
67 "{}/api/softwares/{}/latest-version",
68 self.config.server_url, software_id
69 );
70 let resp = self.auth_get(&url).send().await?;
71 self.decode(resp).await
72 }
73
74 pub async fn download(&self, version_id: &str) -> SdkResult<DownloadInfo> {
75 let url = format!(
76 "{}/api/softwares/sdk-download/{}",
77 self.config.server_url, version_id
78 );
79 let resp = self.auth_post(&url, &serde_json::json!({})).send().await?;
80 self.decode(resp).await
81 }
82
83 pub async fn check_update(
88 &self,
89 platform: &str,
90 current_version: &str,
91 channel: Option<&str>,
92 ) -> SdkResult<UpdateInfo> {
93 let url = format!("{}/api/softwares/sdk-check-update", self.config.server_url);
94 let body = serde_json::json!({
95 "software_id": self.config.software_id,
96 "platform": platform,
97 "channel": channel.unwrap_or("stable"),
98 "current_version": current_version,
99 });
100 let resp = self.auth_post(&url, &body).send().await?;
101 self.decode(resp).await
102 }
103
104 pub async fn open_update_events(
108 &self,
109 software_id: Option<&str>,
110 last_event_id: Option<u64>,
111 ) -> SdkResult<crate::sse::UpdateEventStream> {
112 let stream_client = reqwest::Client::builder().build()?;
114 let url = format!("{}/api/softwares/sdk-update-events", self.config.server_url);
115 let mut req = stream_client
116 .get(&url)
117 .header("Authorization", self.bearer())
118 .header("Accept", "text/event-stream");
119 if let Some(sid) = software_id {
120 req = req.query(&[("software_id", sid)]);
121 }
122 if let Some(last) = last_event_id {
123 req = req.header("Last-Event-ID", last.to_string());
124 }
125 let resp = req.send().await?;
126 if !resp.status().is_success() {
127 return Err(SdkError::ServerError(format!(
128 "SSE connect failed: HTTP {}",
129 resp.status()
130 )));
131 }
132 let stream = futures_util::StreamExt::map(resp.bytes_stream(), |r| {
134 r.map(|b| b.to_vec())
135 });
136 Ok(crate::sse::UpdateEventStream::new(stream))
137 }
138
139 pub async fn list_devices(&self) -> SdkResult<Vec<DeviceRecord>> {
143 let url = format!("{}/api/devices/active", self.config.server_url);
144 let resp = self.auth_get(&url).send().await?;
145 self.decode(resp).await
146 }
147
148 pub async fn unbind_device(&self, device_id: &str) -> SdkResult<()> {
150 let url = format!("{}/api/devices/{}/unbind", self.config.server_url, device_id);
151 let resp = self.auth_post(&url, &serde_json::json!({})).send().await?;
152 let api_resp: ApiResponse<serde_json::Value> = resp.json().await?;
153 if !api_resp.success {
154 return Err(SdkError::ServerError(api_resp.message.unwrap_or_default()));
155 }
156 Ok(())
157 }
158
159 pub async fn fetch_offline_license(&self, fingerprint: &str) -> SdkResult<OfflineLicenseFile> {
165 let url = format!("{}/api/offline-licenses/issue", self.config.server_url);
166 let body = serde_json::json!({
167 "softwareId": self.config.software_id,
168 "fingerprint": fingerprint,
169 });
170 let resp = self.auth_post(&url, &body).send().await?;
171 self.decode(resp).await
172 }
173
174 pub fn verify_offline_license(
177 &self,
178 file: &OfflineLicenseFile,
179 expected_fingerprint: &str,
180 ) -> SdkResult<OfflineLicensePayload> {
181 verify_offline_license(
182 file,
183 &self.config.software_id,
184 expected_fingerprint,
185 self.config.software_public_key.as_deref(),
186 )
187 }
188
189 fn start_heartbeat(&mut self) {
193 if let Some(h) = self.heartbeat_handle.take() {
194 h.abort();
195 }
196 if self.config.heartbeat_interval == 0 {
197 return;
198 }
199 let http = self.http.clone();
200 let config = self.config.clone();
201 let interval = self.config.heartbeat_interval;
202 let handle = tokio::spawn(async move {
203 let mut ticker = tokio::time::interval(Duration::from_secs(interval));
204 loop {
205 ticker.tick().await;
206 let dev = crate::device::collect();
207 let url = format!("{}/api/devices/heartbeat", config.server_url);
208 let _ = http
210 .post(&url)
211 .header("Authorization", format!("Bearer {}", config.pat))
212 .json(&serde_json::json!({
213 "softwareId": config.software_id,
214 "fingerprint": dev.fingerprint,
215 "deviceInfo": dev.info,
216 }))
217 .send()
218 .await;
219 }
220 });
221 self.heartbeat_handle = Some(handle);
222 }
223}
224
225impl Drop for CortexClient {
226 fn drop(&mut self) {
227 if let Some(h) = self.heartbeat_handle.take() {
228 h.abort();
229 }
230 }
231}
232
233pub fn verify_offline_license(
245 file: &OfflineLicenseFile,
246 expected_software_id: &str,
247 expected_fingerprint: &str,
248 pinned_public_key: Option<&str>,
249) -> SdkResult<OfflineLicensePayload> {
250 if let Some(pinned) = pinned_public_key {
252 if pinned.trim() != file.public_key.trim() {
253 return Err(SdkError::LicenseFieldMismatch(format!(
254 "public_key mismatch: pinned {:?} got {:?}",
255 pinned, file.public_key
256 )));
257 }
258 }
259
260 let pub_bytes = hex::decode(&file.public_key).map_err(|e| {
262 SdkError::Signature(format!("invalid public_key hex: {e}"))
263 })?;
264 if pub_bytes.len() != 32 {
265 return Err(SdkError::Signature(format!(
266 "public_key must be 32 bytes, got {}",
267 pub_bytes.len()
268 )));
269 }
270 let mut pk_arr = [0u8; 32];
271 pk_arr.copy_from_slice(&pub_bytes);
272 let vk = ed25519_dalek::VerifyingKey::from_bytes(&pk_arr)
273 .map_err(|e| SdkError::Signature(format!("invalid verifying key: {e}")))?;
274
275 let sig_bytes = hex::decode(&file.signature).map_err(|e| {
276 SdkError::Signature(format!("invalid signature hex: {e}"))
277 })?;
278 if sig_bytes.len() != 64 {
279 return Err(SdkError::Signature(format!(
280 "signature must be 64 bytes, got {}",
281 sig_bytes.len()
282 )));
283 }
284 let sig = ed25519_dalek::Signature::from_slice(&sig_bytes)
285 .map_err(|e| SdkError::Signature(format!("invalid signature: {e}")))?;
286
287 use ed25519_dalek::Verifier;
288 vk
289 .verify(file.payload.as_bytes(), &sig)
290 .map_err(|_| SdkError::Signature("signature verification failed".to_string()))?;
291
292 let payload: OfflineLicensePayload = serde_json::from_str(&file.payload).map_err(|e| {
294 SdkError::Signature(format!("invalid payload json: {e}"))
295 })?;
296
297 if payload.software_id != expected_software_id {
299 return Err(SdkError::LicenseFieldMismatch(format!(
300 "software_id mismatch: expected {expected_software_id:?} got {:?}",
301 payload.software_id
302 )));
303 }
304 if payload.fingerprint != expected_fingerprint {
305 return Err(SdkError::LicenseFieldMismatch(format!(
306 "fingerprint mismatch: expected {expected_fingerprint:?} got {:?}",
307 payload.fingerprint
308 )));
309 }
310
311 let now = std::time::SystemTime::now()
313 .duration_since(std::time::UNIX_EPOCH)
314 .map(|d| d.as_secs() as i64)
315 .unwrap_or(0);
316 if payload.expire_time <= now {
317 return Err(SdkError::LicenseExpired);
318 }
319
320 Ok(payload)
321}