From 7f499d6263b1e1412178b56c5c6861277e51be72 Mon Sep 17 00:00:00 2001 From: ospab Date: Tue, 26 May 2026 21:30:49 +0300 Subject: [PATCH] feat: embed web panel via rust-embed with login page and custom webpath --- .github/workflows/release.yml | 11 ++ Cargo.lock | 286 +++++++++++++++++++++++++++++++++- ostp-server/Cargo.toml | 3 + ostp-server/src/api.rs | 142 +++++++++++++++-- ostp/src/main.rs | 8 +- scripts/build.ps1 | 8 + scripts/install.sh | 56 ++++++- 7 files changed, 492 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 404e6e4..e6c2bb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -120,6 +120,17 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + # ── Frontend Build ───────────────────────────────────────────────────── + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Build Web Panel + working-directory: ostp-control + run: | + npm install + npm run build + # ── Rust toolchain ───────────────────────────────────────────────────── - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/Cargo.lock b/Cargo.lock index d2ab7c1..4461e77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,12 @@ dependencies = [ "syn", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -463,6 +469,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -549,11 +561,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -564,6 +589,21 @@ dependencies = [ "polyval", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -790,6 +830,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -811,6 +857,18 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + [[package]] name = "inout" version = "0.1.4" @@ -906,6 +964,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.185" @@ -957,6 +1021,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mio" version = "1.2.0" @@ -1098,11 +1172,13 @@ dependencies = [ "futures-util", "hmac", "json_comments", + "mime_guess", "ostp-core", "portable-atomic", "rand 0.8.5", "rcgen", "reqwest", + "rust-embed", "rustls", "serde", "serde_json", @@ -1112,6 +1188,7 @@ dependencies = [ "tokio-rustls", "tower-http", "tracing", + "uuid", ] [[package]] @@ -1203,6 +1280,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1282,6 +1369,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -1423,6 +1516,40 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -1949,12 +2076,24 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -1995,6 +2134,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2038,7 +2189,16 @@ version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -2096,6 +2256,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.98" @@ -2359,12 +2553,100 @@ dependencies = [ "toml", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/ostp-server/Cargo.toml b/ostp-server/Cargo.toml index 46fefa3..a2edb1d 100644 --- a/ostp-server/Cargo.toml +++ b/ostp-server/Cargo.toml @@ -23,6 +23,9 @@ sha2.workspace = true base64 = "0.22" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"] } +rust-embed = "8.4" +mime_guess = "2.0" +uuid = { version = "1", features = ["v4", "serde"] } rcgen = "0.13" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } futures-util = "0.3" diff --git a/ostp-server/src/api.rs b/ostp-server/src/api.rs index 72036df..56f35b6 100644 --- a/ostp-server/src/api.rs +++ b/ostp-server/src/api.rs @@ -20,12 +20,15 @@ use portable_atomic::AtomicU64; use std::time::Instant; use axum::{ + body::Body, extract::{Path, State}, - http::StatusCode, - response::IntoResponse, + http::{header, Request, StatusCode, Uri}, + response::{IntoResponse, Response}, routing::{get, post, put}, Json, Router, }; +use rust_embed::RustEmbed; +use sha2::Digest; use serde::{Deserialize, Serialize}; use tower_http::cors::{Any, CorsLayer}; @@ -39,7 +42,10 @@ pub struct ApiState { pub access_keys: Arc>>, pub user_stats: Arc>>>, pub start_time: Instant, - pub api_token: String, + pub session_token: Arc>>, + pub webpath: String, + pub username: String, + pub password_hash: String, /// Server address for subscription links (e.g. "example.com") pub server_host: String, pub server_port: u16, @@ -53,7 +59,12 @@ pub struct ApiState { pub struct ApiConfig { pub enabled: bool, pub bind: String, - pub token: String, + #[serde(default)] + pub webpath: String, + #[serde(default)] + pub username: String, + #[serde(default)] + pub password_hash: String, } impl Default for ApiConfig { @@ -61,7 +72,9 @@ impl Default for ApiConfig { Self { enabled: false, bind: "127.0.0.1:9090".to_string(), - token: String::new(), + webpath: String::new(), + username: String::new(), + password_hash: String::new(), } } } @@ -100,6 +113,17 @@ pub struct SetLimitRequest { pub limit_bytes: Option, } +#[derive(Deserialize)] +pub struct LoginRequest { + pub username: String, + pub password: Option, // We'll accept raw password and hash it +} + +#[derive(Serialize)] +pub struct LoginResponse { + pub token: String, +} + #[derive(Serialize)] struct ApiResponse { ok: bool, @@ -123,6 +147,37 @@ fn api_unauthorized() -> (StatusCode, Json>) { (StatusCode::UNAUTHORIZED, Json(ApiResponse { ok: false, data: None, error: Some("unauthorized".to_string()) })) } +#[derive(RustEmbed)] +#[folder = "../ostp-control/dist/"] +struct Assets; + +async fn static_handler(State(state): State, uri: Uri) -> impl IntoResponse { + let mut path = uri.path(); + let prefix = format!("/{}", state.webpath); + if path.starts_with(&prefix) { + path = &path[prefix.len()..]; + } + path = path.trim_start_matches('/'); + + if path.is_empty() || path == "index.html" { + path = "index.html"; + } + + match Assets::get(path) { + Some(content) => { + let mime = mime_guess::from_path(path).first_or_octet_stream(); + ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() + } + None => { + if let Some(index) = Assets::get("index.html") { + ([(header::CONTENT_TYPE, "text/html")], index.data).into_response() + } else { + (StatusCode::NOT_FOUND, "404 Not Found").into_response() + } + } + } +} + // ── API router ─────────────────────────────────────────────────────────────── pub fn create_api_router(state: ApiState) -> Router { @@ -131,22 +186,37 @@ pub fn create_api_router(state: ApiState) -> Router { .allow_methods(Any) .allow_headers(Any); - Router::new() - .route("/api/server/status", get(handle_status)) - .route("/api/server/config", get(handle_get_config).put(handle_put_config)) + let api_router = Router::new() + .route("/server/status", get(handle_status)) + .route("/server/config", get(handle_get_config).put(handle_put_config)) .route( - "/api/users", + "/users", get(handle_list_users).post(handle_create_user), ) .route( - "/api/users/{key}", + "/users/{key}", get(handle_get_user) .put(update_user) .delete(delete_user), ) - .route("/api/users/{key}/limit", put(handle_set_limit)) - .route("/api/users/{key}/reset", post(handle_reset_stats)) - .route("/api/subscribe/{key}", get(handle_subscribe)) + .route("/users/{key}/limit", put(handle_set_limit)) + .route("/users/{key}/reset", post(handle_reset_stats)) + .route("/subscribe/{key}", get(handle_subscribe)) + .route("/login", post(handle_login)); + + let webpath = state.webpath.clone(); + let webpath = webpath.trim_matches('/'); + + // If no webpath is provided, default to random path to hide panel + let base_route = if webpath.is_empty() { + "/panel".to_string() + } else { + format!("/{}", webpath) + }; + + Router::new() + .nest(&format!("{}/api", base_route), api_router) + .nest(&base_route, Router::new().fallback(get(static_handler))) .layer(cors) .with_state(state) } @@ -165,7 +235,10 @@ pub async fn start_api_server( access_keys, user_stats, start_time: Instant::now(), - api_token: config.token.clone(), + session_token: Arc::new(RwLock::new(None)), + webpath: config.webpath.clone(), + username: config.username.clone(), + password_hash: config.password_hash.clone(), server_host, server_port, reality_query, @@ -192,18 +265,53 @@ pub async fn start_api_server( // ── Middleware: token check ────────────────────────────────────────────────── fn check_token(state: &ApiState, headers: &axum::http::HeaderMap) -> bool { - if state.api_token.is_empty() { - return true; // No auth required if token is empty + // If no credentials configured, panel is open (unsafe but possible) + if state.username.is_empty() && state.password_hash.is_empty() { + return true; } + match headers.get("authorization") { Some(value) => { let val = value.to_str().unwrap_or(""); - val == format!("Bearer {}", state.api_token) || val == state.api_token + if let Some(token) = val.strip_prefix("Bearer ") { + let current_session = state.session_token.read().unwrap().clone(); + if let Some(session) = current_session { + if token == session { + return true; + } + } + } + false } None => false, } } +async fn handle_login( + State(state): State, + Json(payload): Json, +) -> impl IntoResponse { + if state.username.is_empty() || state.password_hash.is_empty() { + return api_error("Auth not configured"); + } + + if payload.username != state.username { + return api_unauthorized::(); + } + + let password = payload.password.unwrap_or_default(); + let hash = sha2::Sha256::digest(password.as_bytes()); + let hash_hex = format!("{:x}", hash); + + if hash_hex == state.password_hash { + let token = uuid::Uuid::new_v4().to_string(); + *state.session_token.write().unwrap() = Some(token.clone()); + (StatusCode::OK, ApiResponse::success(LoginResponse { token })) + } else { + api_unauthorized::() + } +} + fn save_config_keys(state: &ApiState) -> Result<(), String> { let Some(ref path) = state.config_path else { return Ok(()); diff --git a/ostp/src/main.rs b/ostp/src/main.rs index de80903..c6963f0 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -293,7 +293,9 @@ impl ListenConfig { struct ApiConfig { enabled: Option, bind: Option, - token: Option, + webpath: Option, + username: Option, + password_hash: Option, } #[derive(Debug, Deserialize, Serialize)] @@ -861,7 +863,9 @@ async fn run_app() -> Result<()> { let api_config = server_cfg.api.map(|a| ostp_server::ApiConfig { enabled: a.enabled.unwrap_or(false), bind: a.bind.unwrap_or_else(|| "127.0.0.1:9090".to_string()), - token: a.token.unwrap_or_default(), + webpath: a.webpath.unwrap_or_default(), + username: a.username.unwrap_or_default(), + password_hash: a.password_hash.unwrap_or_default(), }); let fallback_config = server_cfg.fallback.map(|f| ostp_server::FallbackConfig { enabled: f.enabled.unwrap_or(false), diff --git a/scripts/build.ps1 b/scripts/build.ps1 index cff7b2f..b2c3088 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -42,6 +42,14 @@ if (Test-Path $CargoToml) { } } +# --- Pre-flight: frontend build --- +Write-Output "" +Write-Output "Building frontend control panel..." +Push-Location (Join-Path $ProjectRoot "ostp-control") +& npm install | Out-Null +& npm run build | Out-Null +Pop-Location + # --- Pre-flight: cargo check --- Write-Output "" Write-Output "Running pre-flight cargo check..." diff --git a/scripts/install.sh b/scripts/install.sh index 6d3dd43..a8fe276 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -199,8 +199,9 @@ echo "Select mode:" echo " 1) Server" echo " 2) Client" echo " 3) Relay" +echo " 4) Server + Web Panel" echo "--------------------------------------------------------" -read -p "Choice [1-3]: " NODE_MODE +read -p "Choice [1-4]: " NODE_MODE cd "$INSTALL_DIR" @@ -242,6 +243,59 @@ with open('$CONFIG_FILE', 'w') as f: echo "" echo "Server configuration saved: $CONFIG_FILE" +elif [ "$NODE_MODE" == "4" ]; then + echo "Initializing server configuration..." + ./ostp --init server --config "$CONFIG_FILE" + + read -p "Listen address [default: 0.0.0.0:50000]: " LISTEN_ADDR + if [ -n "$LISTEN_ADDR" ]; then + sed -i "s/\"listen\": \".*\"/\"listen\": \"$LISTEN_ADDR\"/g" "$CONFIG_FILE" + fi + + # Panel Setup + echo "--- Web Panel Setup ---" + read -p "Panel port [default: 9090]: " PANEL_PORT + PANEL_PORT=${PANEL_PORT:-9090} + + RANDOM_PATH=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 8) + read -p "WebPath (leave empty for random: /$RANDOM_PATH/): " WEBPATH + WEBPATH=${WEBPATH:-$RANDOM_PATH} + + read -p "Username [default: admin]: " USERNAME + USERNAME=${USERNAME:-admin} + + RANDOM_PASS=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 12) + read -p "Password (leave empty for random: $RANDOM_PASS): " PASSWORD + PASSWORD=${PASSWORD:-$RANDOM_PASS} + + # Hash password with python + PASS_HASH=$(python3 -c "import hashlib; print(hashlib.sha256('$PASSWORD'.encode()).hexdigest())") + + # Inject into config + python3 -c " +import json +with open('$CONFIG_FILE') as f: + lines = [l for l in f.read().split('\n') if not l.strip().startswith('//')] + cfg = json.loads('\n'.join(lines)) +if 'api' not in cfg: + cfg['api'] = {} +cfg['api']['enabled'] = True +cfg['api']['bind'] = '0.0.0.0:' + str('$PANEL_PORT') +cfg['api']['webpath'] = '$WEBPATH' +cfg['api']['username'] = '$USERNAME' +cfg['api']['password_hash'] = '$PASS_HASH' +with open('$CONFIG_FILE', 'w') as f: + json.dump(cfg, f, indent=2) +" 2>/dev/null || echo "[warn] Failed to configure panel via python. Edit config manually." + + echo "" + echo "========================================================" + echo "Panel installed successfully!" + echo "URL: http://:$PANEL_PORT/$WEBPATH/" + echo "Username: $USERNAME" + echo "Password: $PASSWORD" + echo "========================================================" + elif [ "$NODE_MODE" == "2" ]; then echo "Initializing client configuration..." ./ostp --init client --config "$CONFIG_FILE"