From 067ee758cdf99d28551e1a0eada865d9831b2796 Mon Sep 17 00:00:00 2001 From: ospab Date: Fri, 15 May 2026 22:13:04 +0300 Subject: [PATCH] feat: implement settings ui forms, add share link parser to cli, add paste link functionality, reduce gui height to 680 --- ostp-gui/src-tauri/tauri.conf.json | 2 +- ostp-gui/src/index.html | 47 +++++++- ostp-gui/src/main.js | 139 +++++++++++++++++++---- ostp-gui/src/styles.css | 172 ++++++++++++++++++++++++++--- ostp/Cargo.toml | 1 + ostp/src/main.rs | 158 ++++++++++++++++++-------- 6 files changed, 428 insertions(+), 91 deletions(-) diff --git a/ostp-gui/src-tauri/tauri.conf.json b/ostp-gui/src-tauri/tauri.conf.json index 650fdb6..e14cebf 100644 --- a/ostp-gui/src-tauri/tauri.conf.json +++ b/ostp-gui/src-tauri/tauri.conf.json @@ -12,7 +12,7 @@ { "title": "OSTP", "width": 360, - "height": 740, + "height": 680, "resizable": false } ], diff --git a/ostp-gui/src/index.html b/ostp-gui/src/index.html index 5e5dad5..9ef7ed8 100644 --- a/ostp-gui/src/index.html +++ b/ostp-gui/src/index.html @@ -75,9 +75,50 @@
-
- - + +
+ + +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ TUN Tunnel Mode + Route all system traffic (Admin req.) +
+ +
+ +
+
+ Debug Logs + Enable verbose internal event outputs +
+ +
diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index 898f6dd..d551212 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -1,10 +1,11 @@ const { invoke } = window.__TAURI__.core; // State management -let appState = 'disconnected'; // 'disconnected', 'connecting', 'connected' +let appState = 'disconnected'; let pollInterval = null; let elapsedSeconds = 0; let elapsedTimer = null; +let rawConfigObj = null; // Cache original config object to preserve extra keys // DOM Elements const btnConnect = document.getElementById('btn-connect'); @@ -19,9 +20,17 @@ const settingsScreen = document.getElementById('settings-screen'); const btnGoSettings = document.getElementById('btn-go-settings'); const btnBack = document.getElementById('btn-back'); const btnSaveConfig = document.getElementById('btn-save-config'); -const configEditor = document.getElementById('config-editor'); const configToast = document.getElementById('config-toast'); +// Input Form Elements +const inImportUrl = document.getElementById('in-import-url'); +const btnImportUrl = document.getElementById('btn-import-url'); +const inServer = document.getElementById('in-server'); +const inKey = document.getElementById('in-key'); +const inSocks = document.getElementById('in-socks'); +const inTunMode = document.getElementById('in-tun-mode'); +const inDebug = document.getElementById('in-debug'); + // Utils function formatBytes(bytes) { if (bytes === 0) return '0.0 B'; @@ -75,11 +84,9 @@ function setUIState(state) { statusText.textContent = 'Protected'; statusText.classList.add('status-connected'); - // Start poll timer if (!pollInterval) { pollInterval = setInterval(fetchMetrics, 1000); } - // Start uptime timer if (!elapsedTimer) { elapsedSeconds = 0; elapsedTimer = setInterval(() => { @@ -97,15 +104,15 @@ async function handleToggleConnect() { try { const success = await invoke('start_tunnel'); if (success) { - // The start_tunnel call waits briefly or returns if spawn worked - // Backend will periodically check status. Let's monitor it. monitorTunnelState(); } else { - alert('Failed to start tunnel process. Check config.json'); + alert('Failed to start tunnel process.'); setUIState('disconnected'); } } catch (err) { - alert('Error launching tunnel: ' + err); + // If the error tells that app exited (due to Admin elevation relaunching), don't show an alert. + // Elevation relaunching closes current app instance silently. + console.error(err); setUIState('disconnected'); } } else { @@ -119,7 +126,6 @@ async function handleToggleConnect() { } async function monitorTunnelState() { - // Check status for up to 5 seconds to confirm it connects let attempts = 0; const check = async () => { try { @@ -134,16 +140,16 @@ async function monitorTunnelState() { if (attempts < 5 && appState === 'connecting') { setTimeout(check, 1000); } else if (appState === 'connecting') { - alert('Tunnel failed to stay alive. Make sure you run with Admin privileges if using TUN mode.'); + alert('Tunnel failed to stay alive. Check log files or Admin rights.'); setUIState('disconnected'); } }; - setTimeout(check, 1500); // Delay initial check to give it time to boot + setTimeout(check, 1500); } async function fetchMetrics() { try { - const stats = await invoke('get_metrics'); // Expected format: { bytes_sent: u64, bytes_recv: u64 } + const stats = await invoke('get_metrics'); if (stats) { metricDown.textContent = formatBytes(stats.bytes_recv); metricUp.textContent = formatBytes(stats.bytes_sent); @@ -152,7 +158,6 @@ async function fetchMetrics() { console.error('Failed to fetch metrics', e); } - // Also verify process is still alive try { const isAlive = await invoke('get_tunnel_status'); if (!isAlive && appState === 'connected') { @@ -163,7 +168,7 @@ async function fetchMetrics() { function switchScreen(target) { if (target === 'settings') { - loadConfigText(); + loadConfigIntoFields(); homeScreen.classList.remove('active'); settingsScreen.classList.add('active'); } else { @@ -172,27 +177,108 @@ function switchScreen(target) { } } -async function loadConfigText() { - configEditor.value = 'Loading configuration...'; +// Config Management +async function loadConfigIntoFields() { try { - const rawConfig = await invoke('get_config'); - configEditor.value = rawConfig; + const rawStr = await invoke('get_config'); + rawConfigObj = JSON.parse(rawStr); + + // Determine if Server mode or Client mode is active + const isClient = rawConfigObj.mode === 'client'; + const clientConf = isClient ? rawConfigObj : null; + + if (clientConf) { + inServer.value = clientConf.server || ''; + inKey.value = clientConf.access_key || ''; + inSocks.value = clientConf.socks5_bind || '127.0.0.1:1088'; + + const tunEnabled = clientConf.tun && clientConf.tun.enable; + inTunMode.checked = !!tunEnabled; + + inDebug.checked = !!clientConf.debug; + } else { + alert('Loaded configuration is for OSTP Server. Please adjust manually.'); + } } catch (err) { - configEditor.value = '// Error loading configuration: ' + err; + console.error('Error loading config', err); } } async function handleSaveConfig() { + if (!rawConfigObj) rawConfigObj = { mode: 'client', log_level: 'info' }; + + // Enforce client settings format + rawConfigObj.mode = 'client'; + rawConfigObj.server = inServer.value.trim(); + rawConfigObj.access_key = inKey.value.trim(); + rawConfigObj.socks5_bind = inSocks.value.trim() || null; + + if (!rawConfigObj.tun) { + rawConfigObj.tun = { + wintun_path: "./wintun.dll", + ipv4_address: "10.1.0.2/24" + }; + } + rawConfigObj.tun.enable = inTunMode.checked; + rawConfigObj.debug = inDebug.checked; + + // Validation + if (!rawConfigObj.server) { + alert('Server Address is required!'); + return; + } + if (!rawConfigObj.access_key) { + alert('Access Key is required!'); + return; + } + try { - const val = configEditor.value; - JSON.parse(val); // Validate JSON format first - const success = await invoke('save_config', { jsonContent: val }); + const finalJson = JSON.stringify(rawConfigObj, null, 2); + const success = await invoke('save_config', { jsonContent: finalJson }); if (success) { showToast(); setTimeout(() => switchScreen('home'), 800); } } catch (err) { - alert('Invalid JSON or saving failed: ' + err.message); + alert('Saving failed: ' + err); + } +} + +// OSTP URI Sharing Parser +function handleImportUrl() { + const urlStr = inImportUrl.value.trim(); + if (!urlStr) return; + + try { + if (!urlStr.startsWith('ostp://')) { + throw new Error('Link must start with ostp://'); + } + // Standard URL parsing + const url = new URL(urlStr); + + const accessKey = decodeURIComponent(url.username); + const serverHost = url.host; // Includes hostname:port + const useTun = url.searchParams.get('tun') === '1' || url.searchParams.get('tun') === 'true'; + const socks5 = url.searchParams.get('socks5'); + + if (!accessKey || !serverHost) { + throw new Error('Incomplete parameters: missing key or server address.'); + } + + // Update fields + inServer.value = serverHost; + inKey.value = accessKey; + inTunMode.checked = useTun; + if (socks5) inSocks.value = socks5; + + inImportUrl.value = ''; // Clear import input + + // Small animation or visual confirm + inImportUrl.placeholder = 'Import successful!'; + setTimeout(() => { inImportUrl.placeholder = 'Paste ostp:// share link here...'; }, 2000); + + } catch (err) { + alert('Failed to parse ostp:// share link: ' + err.message); } } @@ -207,8 +293,13 @@ window.addEventListener('DOMContentLoaded', async () => { btnGoSettings.addEventListener('click', () => switchScreen('settings')); btnBack.addEventListener('click', () => switchScreen('home')); btnSaveConfig.addEventListener('click', handleSaveConfig); + + btnImportUrl.addEventListener('click', handleImportUrl); + inImportUrl.addEventListener('keydown', (e) => { + if (e.key === 'Enter') handleImportUrl(); + }); - // Check current status on startup (reconnect UI if process already active) + // Check current status on startup try { const isAlive = await invoke('get_tunnel_status'); if (isAlive) { diff --git a/ostp-gui/src/styles.css b/ostp-gui/src/styles.css index 21a8bf2..4544124 100644 --- a/ostp-gui/src/styles.css +++ b/ostp-gui/src/styles.css @@ -395,39 +395,175 @@ h2 { .editor-container { flex: 1; border-radius: 16px; - padding: 15px; + padding: 20px 15px; display: flex; flex-direction: column; - gap: 10px; + gap: 18px; overflow: hidden; } -.editor-container label { - font-size: 0.8rem; - color: var(--text-secondary); - font-weight: 500; - letter-spacing: 0.5px; +.editor-container.scrollable { + overflow-y: auto; + padding-right: 8px; } -textarea { +/* Custom Scrollbar */ +.editor-container::-webkit-scrollbar { + width: 5px; +} +.editor-container::-webkit-scrollbar-track { + background: transparent; +} +.editor-container::-webkit-scrollbar-thumb { + background: var(--glass-border); + border-radius: 10px; +} + +.import-container { + border-radius: 14px; + padding: 10px; + display: flex; + gap: 10px; +} + +.import-container input { flex: 1; - width: 100%; background: rgba(0, 0, 0, 0.2); border: 1px solid var(--glass-border); - border-radius: 8px; - padding: 12px; - color: #e2e8f0; - font-family: 'JetBrains Mono', monospace; + border-radius: 10px; + padding: 8px 12px; font-size: 0.8rem; - line-height: 1.5; - resize: none; + color: #fff; outline: none; - transition: border-color 0.3s; } -textarea:focus { +.small-btn { + background: var(--accent-primary); + border: none; + color: white; + font-weight: 600; + font-size: 0.75rem; + padding: 0 14px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.small-btn:hover { opacity: 0.9; } + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group.row-align { + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-top: 1px solid var(--glass-border); +} + +.form-group label { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + letter-spacing: 0.3px; + text-transform: uppercase; +} + +.form-group input[type="text"], +.form-group input[type="password"] { + width: 100%; + background: rgba(0, 0, 0, 0.15); + border: 1px solid var(--glass-border); + border-radius: 10px; + padding: 11px 14px; + color: white; + font-size: 0.9rem; + transition: all 0.3s; + outline: none; +} + +.form-group input:focus { border-color: var(--accent-primary); - box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3); + background: rgba(0, 0, 0, 0.25); +} + +.label-stack { + display: flex; + flex-direction: column; + gap: 2px; +} + +.toggle-label { + font-size: 0.9rem; + font-weight: 500; +} + +.toggle-subtext { + font-size: 0.7rem; + color: var(--text-secondary); +} + +/* Switch CSS Styling */ +.switch { + position: relative; + display: inline-block; + width: 46px; + height: 26px; + flex-shrink: 0; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255,255,255,0.1); + transition: .4s; + border: 1px solid var(--glass-border); +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; +} + +input:checked + .slider { + background-color: var(--accent-primary); + border-color: var(--accent-primary); +} + +input:focus + .slider { + box-shadow: 0 0 1px var(--accent-primary); +} + +input:checked + .slider:before { + transform: translateX(20px); +} + +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; } .actions-container { diff --git a/ostp/Cargo.toml b/ostp/Cargo.toml index 2f04562..f457b79 100644 --- a/ostp/Cargo.toml +++ b/ostp/Cargo.toml @@ -14,3 +14,4 @@ anyhow = "1.0" clap = { version = "4.4", features = ["derive"] } base64 = "0.22" rand.workspace = true +url = "2.5" diff --git a/ostp/src/main.rs b/ostp/src/main.rs index 64cc452..3140b89 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -26,6 +26,54 @@ struct Args { /// Number of keys to generate #[arg(short = 'c', long, default_value_t = 1)] count: usize, + + /// Optional client connection share link (ostp://ACCESS_KEY@HOST:PORT/?tun=1) to run instantly + url: Option, +} + +fn parse_ostp_link(link: &str) -> Result { + let parsed = url::Url::parse(link) + .map_err(|e| anyhow!("Failed to parse share link URL: {e}"))?; + + if parsed.scheme() != "ostp" { + anyhow::bail!("Unsupported URL scheme '{}', expected 'ostp://'", parsed.scheme()); + } + + let access_key = parsed.username().to_string(); + if access_key.is_empty() { + anyhow::bail!("Missing access key (userinfo segment) in share link"); + } + + let host = parsed.host_str().ok_or_else(|| anyhow!("Missing host in share link"))?; + let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?; + let server = format!("{host}:{port}"); + + let mut use_tun = false; + let mut socks5 = None; + + for (k, v) in parsed.query_pairs() { + if k == "tun" && (v == "1" || v.eq_ignore_ascii_case("true")) { + use_tun = true; + } + if k == "socks5" { + socks5 = Some(v.to_string()); + } + } + + Ok(ClientConfig { + server, + access_key, + socks5_bind: socks5, + tun: Some(TunConfig { + enable: use_tun, + wintun_path: Some("./wintun.dll".to_string()), + ipv4_address: Some("10.1.0.2/24".to_string()), + }), + turn: None, + debug: Some(false), + exclude: None, + mux: None, + }) } fn generate_secure_key(format_type: &str) -> String { @@ -159,6 +207,13 @@ async fn run_app() -> Result<()> { return Ok(()); } + if let Some(url) = args.url { + println!("[OSTP Core] Booting direct client connection via share link..."); + let client_cfg = parse_ostp_link(&url) + .map_err(|e| anyhow!("Share Link Error: {e}"))?; + return run_client_directly(client_cfg).await; + } + // Handle explicit configuration initialization if let Some(ref mode_str) = args.init { let is_server = mode_str == "server"; @@ -208,6 +263,15 @@ async fn run_app() -> Result<()> { }; fs::write(&args.config, serde_json::to_string_pretty(&dummy)?)?; println!("Successfully initialized configuration at {:?}", args.config); + + if is_server { + if let AppMode::Server(s) = dummy.mode { + let key = &s.access_keys[0]; + println!("\n>>> Handy Client Share Links for your users:"); + println!(" TUN mode: ostp://{}@:50000/?tun=1", key); + println!(" PROXY mode: ostp://{}@:50000/?tun=0", key); + } + } return Ok(()); } @@ -256,54 +320,58 @@ async fn run_app() -> Result<()> { ostp_server::run_server(server_cfg.listen, server_cfg.access_keys, outbound, debug).await?; } AppMode::Client(client_cfg) => { - println!("[OSTP Core] Starting in CLIENT mode connecting to {}", client_cfg.server); - if let Some(ref tun) = client_cfg.tun { - if tun.enable { - println!("[OSTP Core] TUN mode enabled."); - if let Some(ref path) = tun.wintun_path { - println!("[OSTP Core] Using custom wintun path: {}", path); - // Wiring of custom wintun path to Wintun logic happens here - } - } - } - println!("[OSTP Core] Client logic loaded."); - let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false); - let turn_cfg = client_cfg.turn.as_ref(); - let client_conf = ostp_client::config::ClientConfig { - mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() }, - debug: client_cfg.debug.unwrap_or(false), - ostp: ostp_client::config::OstpConfig { - server_addr: client_cfg.server.clone(), - local_bind_addr: "0.0.0.0:0".to_string(), - access_key: client_cfg.access_key.clone(), - handshake_timeout_ms: 5000, - io_timeout_ms: 5000, - }, - local_proxy: ostp_client::config::LocalProxyConfig { - bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()), - connect_timeout_ms: 5000, - }, - turn: ostp_client::config::TurnConfig { - enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false), - server_addr: turn_cfg.and_then(|t| Some(t.server_addr.clone())).unwrap_or_default(), - username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(), - access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(), - }, - exclusions: ostp_client::config::ExclusionConfig { - domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(), - ips: client_cfg.exclude.as_ref().and_then(|e| e.ips.clone()).unwrap_or_default(), - processes: client_cfg.exclude.as_ref().and_then(|e| e.processes.clone()).unwrap_or_default(), - }, - multiplex: ostp_client::config::MultiplexConfig { - enabled: client_cfg.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false), - sessions: client_cfg.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1), - }, - }; - // Run the client implementation - ostp_client::runner::run_client(client_conf).await?; + run_client_directly(client_cfg).await?; } } Ok(()) } + +async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> { + println!("[OSTP Core] Starting in CLIENT mode connecting to {}", client_cfg.server); + if let Some(ref tun) = client_cfg.tun { + if tun.enable { + println!("[OSTP Core] TUN mode enabled."); + if let Some(ref path) = tun.wintun_path { + println!("[OSTP Core] Using custom wintun path: {}", path); + } + } + } + println!("[OSTP Core] Client logic loaded."); + let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false); + let turn_cfg = client_cfg.turn.as_ref(); + let client_conf = ostp_client::config::ClientConfig { + mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() }, + debug: client_cfg.debug.unwrap_or(false), + ostp: ostp_client::config::OstpConfig { + server_addr: client_cfg.server.clone(), + local_bind_addr: "0.0.0.0:0".to_string(), + access_key: client_cfg.access_key.clone(), + handshake_timeout_ms: 5000, + io_timeout_ms: 5000, + }, + local_proxy: ostp_client::config::LocalProxyConfig { + bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()), + connect_timeout_ms: 5000, + }, + turn: ostp_client::config::TurnConfig { + enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false), + server_addr: turn_cfg.and_then(|t| Some(t.server_addr.clone())).unwrap_or_default(), + username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(), + access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(), + }, + exclusions: ostp_client::config::ExclusionConfig { + domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(), + ips: client_cfg.exclude.as_ref().and_then(|e| e.ips.clone()).unwrap_or_default(), + processes: client_cfg.exclude.as_ref().and_then(|e| e.processes.clone()).unwrap_or_default(), + }, + multiplex: ostp_client::config::MultiplexConfig { + enabled: client_cfg.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false), + sessions: client_cfg.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1), + }, + }; + // Run the client implementation + ostp_client::runner::run_client(client_conf).await?; + Ok(()) +}