use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
const HTML: &str = include_str!("setup.html");
const ICON: &str = include_str!("../assets/icon.svg");
pub fn cmd_setup_web(port: u16, locale: crate::locale::Locale) -> anyhow::Result<()> {
let listener = std::net::TcpListener::bind(format!("127.0.0.1:{port}")).map_err(|e| {
anyhow::anyhow!(
"{} 127.0.0.1:{port}: {e}",
match locale {
crate::locale::Locale::English => "cannot listen on",
crate::locale::Locale::Chinese => "无法监听",
crate::locale::Locale::Japanese => "待受できません",
}
)
})?;
listener.set_nonblocking(true)?;
crate::shutdown::install(locale)?;
let port = listener.local_addr()?.port();
let token = Arc::new(crate::local_http::random_token()?);
let handoff = crate::token_handoff::TokenHandoff::create(&token, locale)?;
let base = format!("http://127.0.0.1:{port}/");
eprintln!(
"[rdsh setup] {}: {base} ({})",
match locale {
crate::locale::Locale::English => "floating UI",
crate::locale::Locale::Chinese => "浮动界面",
crate::locale::Locale::Japanese => "フローティングUI",
},
match locale {
crate::locale::Locale::English => "localhost only, Ctrl-C to stop",
crate::locale::Locale::Chinese => "仅限 localhost,按 Ctrl-C 停止",
crate::locale::Locale::Japanese => "localhost のみ、Ctrl-C で停止",
}
);
eprintln!(
"[rdsh setup] {}: {}",
match locale {
crate::locale::Locale::English => "token handoff file",
crate::locale::Locale::Chinese => "令牌交接文件",
crate::locale::Locale::Japanese => "トークン引き渡しファイル",
},
handoff.path().display()
);
eprintln!(
"[rdsh setup] {}",
match locale {
crate::locale::Locale::English =>
"enter the file contents in the setup page; the token is never printed",
crate::locale::Locale::Chinese => "请在设置页面中输入文件内容;令牌绝不会打印",
crate::locale::Locale::Japanese =>
"セットアップページでファイル内容を入力してください。トークンは表示されません",
}
);
use std::io::IsTerminal as _;
if std::io::stdin().is_terminal() {
if let Err(e) = crate::auth::open_browser(&base, locale) {
eprintln!(
"[rdsh setup] {} ({e:#}); {}",
match locale {
crate::locale::Locale::English => "could not open browser",
crate::locale::Locale::Chinese => "无法打开浏览器",
crate::locale::Locale::Japanese => "ブラウザを開けませんでした",
},
match locale {
crate::locale::Locale::English => "open the URL manually",
crate::locale::Locale::Chinese => "请手动打开 URL",
crate::locale::Locale::Japanese => "URL を手動で開いてください",
}
);
}
}
let done = Arc::new(AtomicBool::new(false));
let connections = Arc::new(AtomicUsize::new(0));
loop {
if done.load(Ordering::Relaxed) || crate::shutdown::requested() {
break;
}
match listener.accept() {
Ok((mut s, _)) => {
let Some(slot) = crate::local_http::ConnectionSlot::acquire(&connections) else {
let _ = crate::local_http::respond(
&mut s,
503,
"application/json",
"{\"error\":\"busy\"}",
);
continue;
};
let d = done.clone();
let token = token.clone();
std::thread::spawn(move || {
let _slot = slot;
if let Err(e) = handle(s, &d, &token, port, locale) {
eprintln!("[rdsh setup] {e:#}");
}
});
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => {
eprintln!("[rdsh setup] accept: {e:#}");
break;
}
}
}
Ok(())
}
fn handle(
mut s: std::net::TcpStream,
done: &AtomicBool,
token: &str,
port: u16,
locale: crate::locale::Locale,
) -> anyhow::Result<()> {
use std::borrow::Cow;
s.set_read_timeout(Some(std::time::Duration::from_secs(10)))?;
let req = match crate::local_http::read_request(&mut s) {
Ok(req) => req,
Err(crate::local_http::ReadRequestError::Idle) => return Ok(()),
Err(crate::local_http::ReadRequestError::Status(status)) => {
crate::local_http::respond(
&mut s,
status,
"application/json",
"{\"error\":\"invalid request\"}",
)?;
return Ok(());
}
};
if !req.trusted(port) {
crate::local_http::respond(
&mut s,
403,
"application/json",
"{\"error\":\"untrusted host or origin\"}",
)?;
return Ok(());
}
let finish_after_response = req.method.as_str() == "POST"
&& req.target.as_str() == "/api/done"
&& req.authorized(token);
if req.method.as_str() == "GET" {
let asset = match req.target.as_str() {
"/icon.png" => Some((
"image/png",
include_bytes!("../assets/icon-256.png").as_slice(),
)),
"/favicon.ico" => Some((
"image/x-icon",
include_bytes!("../assets/icon.ico").as_slice(),
)),
_ => None,
};
if let Some((ctype, bytes)) = asset {
crate::local_http::respond(&mut s, 200, ctype, bytes)?;
return Ok(());
}
}
let (status, ctype, payload): (u16, &str, Cow<str>) =
match (req.method.as_str(), req.target.as_str()) {
("GET", "/") => (
200,
"text/html; charset=utf-8",
Cow::Owned(HTML.replace(
"__RDSH_LOCALE__",
match locale {
crate::locale::Locale::English => "en",
crate::locale::Locale::Chinese => "zh-CN",
crate::locale::Locale::Japanese => "ja",
},
)),
),
("GET", "/icon.svg") => (200, "image/svg+xml", Cow::Borrowed(ICON)),
("GET", "/api/status") if !req.authorized(token) => (
401,
"application/json",
Cow::Borrowed("{\"error\":\"unauthorized\"}"),
),
("GET", "/api/status") => (
200,
"application/json",
Cow::Owned(crate::auth::setup_status_json()),
),
("POST", "/api/key") if !req.authorized(token) => (
401,
"application/json",
Cow::Borrowed("{\"error\":\"unauthorized\"}"),
),
("POST", "/api/key") if req.header("content-type") != Some("application/json") => (
400,
"application/json",
Cow::Borrowed("{\"error\":\"JSON required\"}"),
),
("POST", "/api/key") => match store_key_body(&req.body) {
Ok(stored) => (
200,
"application/json",
Cow::Owned(serde_json::json!({"stored": stored}).to_string()),
),
Err(e) => (
400,
"application/json",
Cow::Owned(serde_json::json!({"error": e.to_string()}).to_string()),
),
},
("GET", "/api/extras") if !req.authorized(token) => (
401,
"application/json",
Cow::Borrowed("{\"error\":\"unauthorized\"}"),
),
("GET", "/api/extras") => {
let cfg = crate::rdsh_config::load();
(
200,
"application/json",
Cow::Owned(
serde_json::json!({"enable": crate::rdsh_config::enabled_extras(&cfg)})
.to_string(),
),
)
}
("POST", "/api/extras") if !req.authorized(token) => (
401,
"application/json",
Cow::Borrowed("{\"error\":\"unauthorized\"}"),
),
("POST", "/api/extras") => match store_extras_body(&req.body) {
Ok(enable) => (
200,
"application/json",
Cow::Owned(serde_json::json!({"enable": enable}).to_string()),
),
Err(e) => (
400,
"application/json",
Cow::Owned(serde_json::json!({"error": e.to_string()}).to_string()),
),
},
("POST", "/api/done") if !req.authorized(token) => (
401,
"application/json",
Cow::Borrowed("{\"error\":\"unauthorized\"}"),
),
("POST", "/api/done") => (200, "application/json", Cow::Borrowed("{\"ok\":true}")),
_ => (
404,
"application/json",
Cow::Borrowed("{\"error\":\"not found\"}"),
),
};
crate::local_http::respond(&mut s, status, ctype, payload.as_bytes())?;
if finish_after_response {
done.store(true, Ordering::Relaxed);
}
Ok(())
}
fn store_extras_body(body: &str) -> anyhow::Result<Vec<String>> {
let v: serde_json::Value = serde_json::from_str(body)?;
let arr = v
.get("enable")
.and_then(|x| x.as_array())
.ok_or_else(|| anyhow::anyhow!("enable must be an array of extra names"))?;
if arr.iter().any(|x| !x.is_string()) {
anyhow::bail!("enable must be an array of extra names");
}
let requested: Vec<String> = arr
.iter()
.filter_map(|x| x.as_str())
.map(|s| s.trim().to_string())
.collect();
let mut enable = Vec::new();
crate::rdsh_config::update(crate::locale::Locale::English, |cfg| {
for id in crate::rdsh_config::KNOWN_EXTRAS {
let on = requested.iter().any(|s| s == id);
if on {
enable.push((*id).to_string());
}
cfg.set_dotted(&format!("features.{id}"), if on { "true" } else { "false" })?;
}
cfg.extras.enable = enable.clone();
Ok(())
})?;
Ok(enable)
}
fn store_key_body(body: &str) -> anyhow::Result<bool> {
let v: serde_json::Value = serde_json::from_str(body)?;
let name = v.get("name").and_then(|x| x.as_str()).unwrap_or("");
let value = v.get("value").and_then(|x| x.as_str()).unwrap_or("");
crate::auth::setup_store_key(name, value)
}