-
-
+
+
+
+
+
+
+
+
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(())
+}