tauri_plugin_automation_server/
server.rs1use tauri::{AppHandle, Manager};
6use tiny_http::{Header, Method, Response, Server};
7
8use crate::take_screenshot_data;
9
10const PORT: u16 = 9876;
11
12pub 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}