Skip to main content

headless_engine/api/
rpc.rs

1use crate::browser::engine::BrowserEngine;
2use crate::network::fingerprint::DeviceProfile;
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Deserialize, Serialize, Clone)]
7pub struct RpcRequest {
8    pub jsonrpc: Option<String>,
9    pub id: Option<serde_json::Value>,
10    pub method: String,
11    #[serde(default)]
12    pub params: serde_json::Value,
13}
14
15#[derive(Debug, Serialize, Deserialize, Clone)]
16pub struct RpcError {
17    pub code: i32,
18    pub message: String,
19}
20
21#[derive(Debug, Serialize, Deserialize, Clone)]
22pub struct RpcResponse {
23    pub jsonrpc: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub id: Option<serde_json::Value>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub result: Option<serde_json::Value>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub error: Option<RpcError>,
30}
31
32impl RpcResponse {
33    pub fn success(id: Option<serde_json::Value>, result: serde_json::Value) -> Self {
34        Self {
35            jsonrpc: "2.0".to_string(),
36            id,
37            result: Some(result),
38            error: None,
39        }
40    }
41
42    pub fn error(id: Option<serde_json::Value>, code: i32, message: impl Into<String>) -> Self {
43        Self {
44            jsonrpc: "2.0".to_string(),
45            id,
46            result: None,
47            error: Some(RpcError {
48                code,
49                message: message.into(),
50            }),
51        }
52    }
53}
54
55pub struct JsonRpcHandler {
56    pub engine: BrowserEngine,
57}
58
59impl JsonRpcHandler {
60    pub fn new() -> Result<Self> {
61        Ok(Self {
62            engine: BrowserEngine::new()?,
63        })
64    }
65
66    pub async fn handle_line(&mut self, line: &str) -> RpcResponse {
67        let req: RpcRequest = match serde_json::from_str(line) {
68            Ok(r) => r,
69            Err(e) => return RpcResponse::error(None, -32700, format!("Parse error: {}", e)),
70        };
71
72        self.handle_request(req).await
73    }
74
75    pub async fn handle_request(&mut self, req: RpcRequest) -> RpcResponse {
76        let id = req.id.clone();
77        let method = req.method.as_str();
78        let params = &req.params;
79
80        match method {
81            // Engine Methods
82            "engine.createTab" | "createTab" => {
83                let profile: Option<DeviceProfile> = params
84                    .get("profile")
85                    .and_then(|p| serde_json::from_value(p.clone()).ok());
86                match self.engine.create_tab(profile) {
87                    Ok(tab_id) => RpcResponse::success(id, serde_json::json!({ "tab_id": tab_id })),
88                    Err(e) => RpcResponse::error(id, -32000, format!("CreateTab error: {}", e)),
89                }
90            }
91            "engine.closeTab" | "closeTab" => {
92                let tab_id = params.get("tab_id").and_then(|v| v.as_str()).unwrap_or("");
93                let closed = self.engine.close_tab(tab_id);
94                RpcResponse::success(id, serde_json::json!({ "closed": closed }))
95            }
96            "engine.listTabs" | "listTabs" => {
97                let tabs = self.engine.list_tabs();
98                RpcResponse::success(id, serde_json::json!({ "tabs": tabs, "count": tabs.len() }))
99            }
100
101            // Tab Methods (with optional tab_id)
102            "tab.navigate" | "navigate" | "Navigate" => {
103                let url = params.get("url").and_then(|v| v.as_str()).unwrap_or("");
104                if url.is_empty() {
105                    return RpcResponse::error(id, -32602, "Missing 'url' parameter");
106                }
107                let tab = match self.get_target_tab_mut(params) {
108                    Ok(t) => t,
109                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
110                };
111                match tab.navigate(url).await {
112                    Ok(report) => match serde_json::to_value(report) {
113                        Ok(v) => RpcResponse::success(id, v),
114                        Err(e) => {
115                            RpcResponse::error(id, -32000, format!("Serialization failed: {}", e))
116                        }
117                    },
118                    Err(e) => RpcResponse::error(id, -32000, format!("Navigation failed: {}", e)),
119                }
120            }
121            "tab.observe" | "observe" | "Observe" => {
122                let tab = match self.get_target_tab_mut(params) {
123                    Ok(t) => t,
124                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
125                };
126                match tab.observe() {
127                    Some(obs) => match serde_json::to_value(obs) {
128                        Ok(v) => RpcResponse::success(id, v),
129                        Err(e) => {
130                            RpcResponse::error(id, -32000, format!("Serialization failed: {}", e))
131                        }
132                    },
133                    None => RpcResponse::error(id, -32000, "No page loaded. Call navigate first."),
134                }
135            }
136            "tab.extractInteractive" | "extractInteractive" => {
137                let tab = match self.get_target_tab_mut(params) {
138                    Ok(t) => t,
139                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
140                };
141                let elements = tab.extract_interactive_elements();
142                RpcResponse::success(
143                    id,
144                    serde_json::json!({ "elements": elements, "count": elements.len() }),
145                )
146            }
147            "tab.screenshot" | "screenshot" | "Screenshot" => {
148                let tab = match self.get_target_tab_mut(params) {
149                    Ok(t) => t,
150                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
151                };
152                match tab.screenshot_async().await {
153                    Some(shot) => match serde_json::to_value(shot) {
154                        Ok(v) => RpcResponse::success(id, v),
155                        Err(e) => {
156                            RpcResponse::error(id, -32000, format!("Serialization failed: {}", e))
157                        }
158                    },
159                    None => RpcResponse::error(id, -32000, "No page loaded. Call navigate first."),
160                }
161            }
162            "tab.screenshotLayout" | "screenshotLayout" => {
163                let tab = match self.get_target_tab_mut(params) {
164                    Ok(t) => t,
165                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
166                };
167                match tab.screenshot_layout() {
168                    Some(layout) => {
169                        RpcResponse::success(id, serde_json::json!({ "layout": layout }))
170                    }
171                    None => RpcResponse::error(id, -32000, "No page loaded. Call navigate first."),
172                }
173            }
174            "tab.screenshotSvg" | "screenshotSvg" => {
175                let tab = match self.get_target_tab_mut(params) {
176                    Ok(t) => t,
177                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
178                };
179                match tab.screenshot_svg() {
180                    Some(svg) => RpcResponse::success(id, serde_json::json!({ "svg": svg })),
181                    None => RpcResponse::error(id, -32000, "No page loaded. Call navigate first."),
182                }
183            }
184            "tab.extractMarkdown" | "extractMarkdown" | "ExtractMarkdown" => {
185                let selector = params.get("selector").and_then(|v| v.as_str());
186                let tab = match self.get_target_tab_mut(params) {
187                    Ok(t) => t,
188                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
189                };
190                match tab.extract_markdown(selector) {
191                    Some(markdown) => RpcResponse::success(
192                        id,
193                        serde_json::json!({ "markdown": markdown, "length": markdown.len() }),
194                    ),
195                    None => RpcResponse::error(id, -32000, "No DOM loaded. Call navigate first."),
196                }
197            }
198            "tab.extractResults" | "extractResults" | "ExtractResults" => {
199                let tab = match self.get_target_tab_mut(params) {
200                    Ok(t) => t,
201                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
202                };
203                match tab.extract_search_results() {
204                    Some(results) => match serde_json::to_value(results) {
205                        Ok(v) => RpcResponse::success(id, v),
206                        Err(e) => {
207                            RpcResponse::error(id, -32000, format!("Serialization failed: {}", e))
208                        }
209                    },
210                    None => RpcResponse::error(
211                        id,
212                        -32000,
213                        "No search results available. Call navigate first.",
214                    ),
215                }
216            }
217            "tab.extractLinks" | "extractLinks" | "ExtractLinks" => {
218                let tab = match self.get_target_tab_mut(params) {
219                    Ok(t) => t,
220                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
221                };
222                let links = tab.extract_links();
223                RpcResponse::success(
224                    id,
225                    serde_json::json!({ "links": links, "count": links.len() }),
226                )
227            }
228            "tab.extractForms" | "extractForms" | "ExtractForms" => {
229                let tab = match self.get_target_tab_mut(params) {
230                    Ok(t) => t,
231                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
232                };
233                let forms = tab.extract_forms();
234                RpcResponse::success(
235                    id,
236                    serde_json::json!({ "forms": forms, "count": forms.len() }),
237                )
238            }
239            "tab.extractDom" | "extractDom" | "ExtractDom" => {
240                let selector = params.get("selector").and_then(|v| v.as_str());
241                let tab = match self.get_target_tab_mut(params) {
242                    Ok(t) => t,
243                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
244                };
245                match tab.extract_dom(selector) {
246                    Some(html) => RpcResponse::success(
247                        id,
248                        serde_json::json!({ "html": html, "length": html.len() }),
249                    ),
250                    None => RpcResponse::error(id, -32000, "No DOM loaded. Call navigate first."),
251                }
252            }
253            "tab.click" | "click" | "Click" | "tab.actClick" => {
254                let target = params
255                    .get("target")
256                    .or_else(|| params.get("index"))
257                    .map(|v| {
258                        if v.is_number() {
259                            v.to_string()
260                        } else {
261                            v.as_str().unwrap_or("").to_string()
262                        }
263                    })
264                    .unwrap_or_default();
265                if target.is_empty() {
266                    return RpcResponse::error(id, -32602, "Missing 'target' or 'index' parameter");
267                }
268                let tab = match self.get_target_tab_mut(params) {
269                    Ok(t) => t,
270                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
271                };
272                match tab.act_click(&target).await {
273                    Ok(Some(report)) => RpcResponse::success(
274                        id,
275                        serde_json::json!({ "action": "navigated", "report": report }),
276                    ),
277                    Ok(None) => {
278                        RpcResponse::success(id, serde_json::json!({ "action": "clicked_in_page" }))
279                    }
280                    Err(e) => RpcResponse::error(id, -32000, format!("Click error: {}", e)),
281                }
282            }
283            "tab.type" | "type" | "Type" | "tab.actType" => {
284                let selector = params
285                    .get("selector")
286                    .or_else(|| params.get("target"))
287                    .or_else(|| params.get("index"))
288                    .map(|v| {
289                        if v.is_number() {
290                            v.to_string()
291                        } else {
292                            v.as_str().unwrap_or("").to_string()
293                        }
294                    })
295                    .unwrap_or_default();
296                let text = params.get("text").and_then(|v| v.as_str()).unwrap_or("");
297                let tab = match self.get_target_tab_mut(params) {
298                    Ok(t) => t,
299                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
300                };
301                match tab.act_type(&selector, text).await {
302                    Ok(res) => RpcResponse::success(id, serde_json::json!({ "status": res })),
303                    Err(e) => RpcResponse::error(id, -32000, format!("Type error: {}", e)),
304                }
305            }
306            "tab.evaluateJs" | "evaluateJs" | "EvaluateJs" => {
307                let code = params.get("code").and_then(|v| v.as_str()).unwrap_or("");
308                let tab = match self.get_target_tab_mut(params) {
309                    Ok(t) => t,
310                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
311                };
312                match tab.evaluate_js(code) {
313                    Ok(res) => RpcResponse::success(id, serde_json::json!({ "result": res })),
314                    Err(e) => RpcResponse::error(id, -32000, format!("JS Error: {}", e)),
315                }
316            }
317            "tab.setProfile" | "setProfile" | "SetProfile" => {
318                let profile: DeviceProfile = match params
319                    .get("profile")
320                    .and_then(|p| serde_json::from_value(p.clone()).ok())
321                {
322                    Some(p) => p,
323                    None => {
324                        return RpcResponse::error(
325                            id,
326                            -32602,
327                            "Invalid or missing 'profile' parameter",
328                        )
329                    }
330                };
331                let tab = match self.get_target_tab_mut(params) {
332                    Ok(t) => t,
333                    Err(e) => return RpcResponse::error(id, -32000, e.to_string()),
334                };
335                match tab.set_profile(profile) {
336                    Ok(_) => RpcResponse::success(
337                        id,
338                        serde_json::json!({ "status": "profile_updated", "profile": profile }),
339                    ),
340                    Err(e) => RpcResponse::error(id, -32000, format!("SetProfile error: {}", e)),
341                }
342            }
343            "ping" => RpcResponse::success(id, serde_json::json!({ "pong": true })),
344            "shutdown" | "Shutdown" => {
345                RpcResponse::success(id, serde_json::json!({ "status": "shutting down" }))
346            }
347            _ => RpcResponse::error(id, -32601, format!("Method not found: {}", method)),
348        }
349    }
350
351    fn get_target_tab_mut(
352        &mut self,
353        params: &serde_json::Value,
354    ) -> Result<&mut crate::browser::tab::BrowserTab> {
355        if let Some(tab_id) = params.get("tab_id").and_then(|v| v.as_str()) {
356            self.engine
357                .get_tab_mut(tab_id)
358                .ok_or_else(|| anyhow::anyhow!("Tab '{}' not found", tab_id))
359        } else {
360            self.engine.default_tab_mut()
361        }
362    }
363}