Skip to main content

tauri_plugin_automation_server/
server.rs

1//! HTTP server for automation commands
2//!
3//! Provides HTTP API on port 9876 for external automation.
4
5use tauri::{AppHandle, Manager};
6use tiny_http::{Header, Method, Response, Server};
7
8use crate::take_screenshot_data;
9
10const PORT: u16 = 9876;
11
12/// Run the HTTP automation server
13pub fn run_server(app_handle: AppHandle) {
14    let addr = format!("127.0.0.1:{}", PORT);
15
16    let server = match Server::http(&addr) {
17        Ok(s) => s,
18        Err(e) => {
19            eprintln!("[Automation] Failed to start server on {}: {}", addr, e);
20            return;
21        }
22    };
23
24    println!("[Automation] HTTP server listening on http://{}", addr);
25
26    for mut request in server.incoming_requests() {
27        let method = request.method().clone();
28        let url = request.url().to_string();
29
30        println!("[Automation] {} {}", method, url);
31
32        let response = match (&method, url.as_str()) {
33            (&Method::Get, "/automation/health") => json_response(serde_json::json!({
34                "status": "ok",
35                "port": PORT,
36                "version": "0.2.0"
37            })),
38
39            (&Method::Post, "/automation/execute") => handle_execute(&app_handle, &mut request),
40
41            (&Method::Get, "/automation/screenshot") => handle_screenshot(&app_handle),
42
43            (&Method::Options, _) => cors_response(),
44
45            _ => json_response_with_status(serde_json::json!({ "error": "Not found" }), 404),
46        };
47
48        if let Err(e) = request.respond(response) {
49            eprintln!("[Automation] Failed to send response: {}", e);
50        }
51    }
52}
53
54fn handle_execute(
55    app_handle: &AppHandle,
56    request: &mut tiny_http::Request,
57) -> Response<std::io::Cursor<Vec<u8>>> {
58    let mut body = String::new();
59    if let Err(e) = request.as_reader().read_to_string(&mut body) {
60        return json_response_with_status(
61            serde_json::json!({ "error": format!("Failed to read body: {}", e) }),
62            400,
63        );
64    }
65
66    let payload: serde_json::Value = match serde_json::from_str(&body) {
67        Ok(v) => v,
68        Err(e) => {
69            return json_response_with_status(
70                serde_json::json!({ "error": format!("Invalid JSON: {}", e) }),
71                400,
72            );
73        }
74    };
75
76    let command = match payload.get("command").and_then(|v| v.as_str()) {
77        Some(c) => c.to_string(),
78        None => {
79            return json_response_with_status(
80                serde_json::json!({ "error": "Missing 'command' field" }),
81                400,
82            );
83        }
84    };
85
86    let args = payload.get("args").cloned().unwrap_or(serde_json::json!({}));
87
88    let window = match app_handle.get_webview_window("main") {
89        Some(w) => w,
90        None => {
91            return json_response_with_status(
92                serde_json::json!({ "error": "Main window not found" }),
93                500,
94            );
95        }
96    };
97
98    let args_json = serde_json::to_string(&args).unwrap_or_else(|_| "{}".to_string());
99    let script = format!(
100        r#"
101        (async function() {{
102            if (typeof window.__TAURI_AUTOMATION__ === 'undefined') {{
103                console.error('[Automation] Not initialized');
104                return;
105            }}
106            try {{
107                const result = await window.__TAURI_AUTOMATION__.execute('{}', {});
108                window.__TAURI_AUTOMATION__._lastResult = {{ success: true, result: result }};
109            }} catch (e) {{
110                window.__TAURI_AUTOMATION__._lastResult = {{ success: false, error: e.message || String(e) }};
111            }}
112        }})();
113        "#,
114        command, args_json
115    );
116
117    if let Err(e) = window.eval(&script) {
118        return json_response_with_status(
119            serde_json::json!({ "error": format!("Script execution failed: {}", e) }),
120            500,
121        );
122    }
123
124    std::thread::sleep(std::time::Duration::from_millis(100));
125
126    json_response(serde_json::json!({
127        "success": true,
128        "message": "Command executed",
129        "command": command
130    }))
131}
132
133fn handle_screenshot(app_handle: &AppHandle) -> Response<std::io::Cursor<Vec<u8>>> {
134    let window = match app_handle.get_webview_window("main") {
135        Some(w) => w,
136        None => {
137            return json_response_with_status(
138                serde_json::json!({ "error": "Main window not found" }),
139                500,
140            );
141        }
142    };
143
144    let script = r#"
145        (async function() {
146            if (typeof window.__TAURI_AUTOMATION__ === 'undefined') {
147                console.error('[Automation] Not initialized');
148                return;
149            }
150            try {
151                await window.__TAURI_AUTOMATION__.captureAndSend();
152            } catch (e) {
153                console.error('[Automation] Screenshot failed:', e);
154            }
155        })();
156    "#;
157
158    if let Err(e) = window.eval(script) {
159        return json_response_with_status(
160            serde_json::json!({ "error": format!("Screenshot request failed: {}", e) }),
161            500,
162        );
163    }
164
165    std::thread::sleep(std::time::Duration::from_millis(2000));
166
167    if let Some(data_url) = take_screenshot_data() {
168        if let Some(base64_data) = data_url.strip_prefix("data:image/png;base64,") {
169            match base64_decode(base64_data) {
170                Ok(bytes) => return png_response(bytes),
171                Err(e) => {
172                    return json_response_with_status(
173                        serde_json::json!({ "error": format!("Base64 decode failed: {}", e) }),
174                        500,
175                    );
176                }
177            }
178        }
179    }
180
181    json_response_with_status(
182        serde_json::json!({ "error": "Screenshot not available. Make sure html2canvas is loaded." }),
183        500,
184    )
185}
186
187fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
188    let input = input.trim();
189    let chars: Vec<char> = input.chars().filter(|c| !c.is_whitespace()).collect();
190
191    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
192
193    let mut output = Vec::new();
194    let mut buffer: u32 = 0;
195    let mut bits_collected = 0;
196
197    for c in chars {
198        if c == '=' {
199            break;
200        }
201
202        let value = ALPHABET
203            .iter()
204            .position(|&x| x == c as u8)
205            .ok_or_else(|| format!("Invalid base64 character: {}", c))? as u32;
206
207        buffer = (buffer << 6) | value;
208        bits_collected += 6;
209
210        if bits_collected >= 8 {
211            bits_collected -= 8;
212            output.push((buffer >> bits_collected) as u8);
213            buffer &= (1 << bits_collected) - 1;
214        }
215    }
216
217    Ok(output)
218}
219
220fn json_response(data: serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
221    json_response_with_status(data, 200)
222}
223
224fn json_response_with_status(
225    data: serde_json::Value,
226    status: u16,
227) -> Response<std::io::Cursor<Vec<u8>>> {
228    let body = serde_json::to_vec(&data).unwrap_or_else(|_| b"{}".to_vec());
229    let len = body.len();
230    let cursor = std::io::Cursor::new(body);
231
232    Response::new(
233        tiny_http::StatusCode(status),
234        vec![
235            Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
236            Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
237            Header::from_bytes(&b"Access-Control-Allow-Methods"[..], &b"GET, POST, OPTIONS"[..])
238                .unwrap(),
239            Header::from_bytes(&b"Access-Control-Allow-Headers"[..], &b"Content-Type"[..]).unwrap(),
240        ],
241        cursor,
242        Some(len),
243        None,
244    )
245}
246
247fn png_response(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {
248    let len = data.len();
249    let cursor = std::io::Cursor::new(data);
250
251    Response::new(
252        tiny_http::StatusCode(200),
253        vec![
254            Header::from_bytes(&b"Content-Type"[..], &b"image/png"[..]).unwrap(),
255            Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
256        ],
257        cursor,
258        Some(len),
259        None,
260    )
261}
262
263fn cors_response() -> Response<std::io::Cursor<Vec<u8>>> {
264    let cursor = std::io::Cursor::new(Vec::new());
265
266    Response::new(
267        tiny_http::StatusCode(204),
268        vec![
269            Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
270            Header::from_bytes(&b"Access-Control-Allow-Methods"[..], &b"GET, POST, OPTIONS"[..])
271                .unwrap(),
272            Header::from_bytes(&b"Access-Control-Allow-Headers"[..], &b"Content-Type"[..]).unwrap(),
273        ],
274        cursor,
275        Some(0),
276        None,
277    )
278}