A variante clara tinha artefatos; a arte escura recortada serve aos dois temas. Saem a troca de variante por tema, os arquivos -light e a fonte clara; os arquivos escuros viram guara.png e guara-s.png e o script, o app, o servidor do guara ui e o README usam só eles. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
162 lines
6.5 KiB
Rust
162 lines
6.5 KiB
Rust
//! Servidor local da interface (127.0.0.1 + token): mesma UI do app Tauri.
|
|
|
|
use guara_core::service::Engine;
|
|
use serde_json::json;
|
|
use std::io::Read;
|
|
use std::sync::Arc;
|
|
use tiny_http::{Header, Method, Request, Response, Server};
|
|
|
|
const INDEX: &str = include_str!("../../../apps/gui/ui/index.html");
|
|
const CSS: &str = include_str!("../../../apps/gui/ui/app.css");
|
|
const JS: &str = include_str!("../../../apps/gui/ui/app.js");
|
|
const PREVIEW_JS: &str = include_str!("../../../apps/gui/ui/preview.js");
|
|
const I18N_JS: &str = include_str!("../../../apps/gui/ui/i18n.js");
|
|
const I18N_EN_JS: &str = include_str!("../../../apps/gui/ui/i18n-en.js");
|
|
const EXPLORER_JS: &str = include_str!("../../../apps/gui/ui/explorer.js");
|
|
const LOGO: &[u8] = include_bytes!("../../../apps/gui/ui/guara.png");
|
|
const LOGO_S: &[u8] = include_bytes!("../../../apps/gui/ui/guara-s.png");
|
|
const PDF_JS: &str = include_str!("../../../apps/gui/ui/vendor/pdf.min.mjs");
|
|
const PDF_WORKER: &str = include_str!("../../../apps/gui/ui/vendor/pdf.worker.min.mjs");
|
|
|
|
pub fn run(port: u16, open: bool) -> Result<(), String> {
|
|
let server = Server::http(("127.0.0.1", port)).map_err(|e| e.to_string())?;
|
|
let real_port = server
|
|
.server_addr()
|
|
.to_ip()
|
|
.map(|a| a.port())
|
|
.unwrap_or(port);
|
|
let mut tok = [0u8; 16];
|
|
getrandom::fill(&mut tok).map_err(|e| e.to_string())?;
|
|
let token: Arc<str> = tok
|
|
.iter()
|
|
.map(|b| format!("{b:02x}"))
|
|
.collect::<String>()
|
|
.into();
|
|
let url = format!("http://127.0.0.1:{real_port}/?token={token}");
|
|
eprintln!("Guará UI em {url}\n(Ctrl+C para encerrar. Para ler discos brutos, rode como administrador/root.)");
|
|
if open {
|
|
open_browser(&url);
|
|
}
|
|
let eng = Arc::new(Engine::new());
|
|
let hosts: Arc<[String; 2]> = Arc::new([
|
|
format!("127.0.0.1:{real_port}"),
|
|
format!("localhost:{real_port}"),
|
|
]);
|
|
for req in server.incoming_requests() {
|
|
let (eng, token, hosts) = (eng.clone(), token.clone(), hosts.clone());
|
|
// Uma thread por requisição: vídeo/áudio em streaming não pode travar a interface.
|
|
std::thread::spawn(move || handle(req, &eng, &token, &hosts[..]));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn header(name: &str, value: &str) -> Header {
|
|
Header::from_bytes(name.as_bytes(), value.as_bytes()).unwrap()
|
|
}
|
|
|
|
fn handle(req: Request, eng: &Engine, token: &str, hosts: &[String]) {
|
|
let host_ok = req
|
|
.headers()
|
|
.iter()
|
|
.any(|h| h.field.equiv("Host") && hosts.iter().any(|x| x == h.value.as_str()));
|
|
if !host_ok {
|
|
let _ = req.respond(Response::from_string("host inválido").with_status_code(403));
|
|
return;
|
|
}
|
|
let Some(mut req) = guara_core::http::handle_media(eng, token, req) else {
|
|
return;
|
|
};
|
|
let full = req.url().to_string();
|
|
let (path, _query) = full.split_once('?').unwrap_or((full.as_str(), ""));
|
|
let ct = |v: &str| header("Content-Type", v);
|
|
match (req.method().clone(), path) {
|
|
(Method::Get, "/") | (Method::Get, "/index.html") => {
|
|
let _ = req
|
|
.respond(Response::from_string(INDEX).with_header(ct("text/html; charset=utf-8")));
|
|
}
|
|
(Method::Get, "/app.css") => {
|
|
let _ =
|
|
req.respond(Response::from_string(CSS).with_header(ct("text/css; charset=utf-8")));
|
|
}
|
|
(Method::Get, "/app.js") => {
|
|
let _ = req.respond(
|
|
Response::from_string(JS).with_header(ct("text/javascript; charset=utf-8")),
|
|
);
|
|
}
|
|
(Method::Get, "/preview.js") => {
|
|
let _ = req.respond(
|
|
Response::from_string(PREVIEW_JS).with_header(ct("text/javascript; charset=utf-8")),
|
|
);
|
|
}
|
|
(Method::Get, "/i18n.js") => {
|
|
let _ = req.respond(
|
|
Response::from_string(I18N_JS).with_header(ct("text/javascript; charset=utf-8")),
|
|
);
|
|
}
|
|
(Method::Get, "/i18n-en.js") => {
|
|
let _ = req.respond(
|
|
Response::from_string(I18N_EN_JS).with_header(ct("text/javascript; charset=utf-8")),
|
|
);
|
|
}
|
|
(Method::Get, "/guara.png") => {
|
|
let _ = req.respond(Response::from_data(LOGO).with_header(ct("image/png")));
|
|
}
|
|
(Method::Get, "/guara-s.png") => {
|
|
let _ = req.respond(Response::from_data(LOGO_S).with_header(ct("image/png")));
|
|
}
|
|
(Method::Get, "/explorer.js") => {
|
|
let _ = req.respond(
|
|
Response::from_string(EXPLORER_JS)
|
|
.with_header(ct("text/javascript; charset=utf-8")),
|
|
);
|
|
}
|
|
(Method::Get, "/vendor/pdf.min.mjs") => {
|
|
let _ = req.respond(
|
|
Response::from_string(PDF_JS).with_header(ct("text/javascript; charset=utf-8")),
|
|
);
|
|
}
|
|
(Method::Get, "/vendor/pdf.worker.min.mjs") => {
|
|
let _ = req.respond(
|
|
Response::from_string(PDF_WORKER).with_header(ct("text/javascript; charset=utf-8")),
|
|
);
|
|
}
|
|
(Method::Post, p) if p.starts_with("/rpc/") => {
|
|
let ok = req
|
|
.headers()
|
|
.iter()
|
|
.any(|h| h.field.equiv("X-Guara-Token") && h.value.as_str() == token);
|
|
if !ok {
|
|
let _ = req.respond(Response::from_string("token inválido").with_status_code(401));
|
|
return;
|
|
}
|
|
let method = p.trim_start_matches("/rpc/").to_string();
|
|
let mut body = String::new();
|
|
let _ = req.as_reader().take(1 << 20).read_to_string(&mut body);
|
|
let params = serde_json::from_str(&body).unwrap_or(json!({}));
|
|
let (code, out) = match eng.rpc(&method, params) {
|
|
Ok(v) => (200, v),
|
|
Err(e) => (400, json!({ "error": e.to_string() })),
|
|
};
|
|
let _ = req.respond(
|
|
Response::from_string(out.to_string())
|
|
.with_status_code(code)
|
|
.with_header(ct("application/json")),
|
|
);
|
|
}
|
|
_ => {
|
|
let _ = req.respond(Response::from_string("não encontrado").with_status_code(404));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn open_browser(url: &str) {
|
|
#[cfg(target_os = "linux")]
|
|
let _ = std::process::Command::new("xdg-open").arg(url).spawn();
|
|
#[cfg(target_os = "macos")]
|
|
let _ = std::process::Command::new("open").arg(url).spawn();
|
|
#[cfg(windows)]
|
|
let _ = std::process::Command::new("cmd")
|
|
.args(["/C", "start", "", url])
|
|
.spawn();
|
|
}
|