feat: implement settings ui forms, add share link parser to cli, add paste link functionality, reduce gui height to 680

This commit is contained in:
ospab 2026-05-15 22:13:04 +03:00
parent b26863e8e5
commit 067ee758cd
6 changed files with 428 additions and 91 deletions

View File

@ -12,7 +12,7 @@
{
"title": "OSTP",
"width": 360,
"height": 740,
"height": 680,
"resizable": false
}
],

View File

@ -75,9 +75,50 @@
</header>
<div class="settings-content">
<div class="editor-container glass">
<label for="config-editor">Client JSON Config</label>
<textarea id="config-editor" spellcheck="false" placeholder="Loading configuration..."></textarea>
<!-- Import Area -->
<div class="import-container glass">
<input type="text" id="in-import-url" placeholder="Paste ostp:// share link here..." />
<button id="btn-import-url" class="small-btn">Import</button>
</div>
<!-- Form Settings -->
<div class="editor-container glass scrollable">
<div class="form-group">
<label for="in-server">Server Address</label>
<input type="text" id="in-server" placeholder="127.0.0.1:50000" />
</div>
<div class="form-group">
<label for="in-key">Access Key</label>
<input type="password" id="in-key" placeholder="Enter secure access key" />
</div>
<div class="form-group">
<label for="in-socks">SOCKS5 Bind Address</label>
<input type="text" id="in-socks" placeholder="127.0.0.1:1088" />
</div>
<div class="form-group row-align">
<div class="label-stack">
<span class="toggle-label">TUN Tunnel Mode</span>
<span class="toggle-subtext">Route all system traffic (Admin req.)</span>
</div>
<label class="switch">
<input type="checkbox" id="in-tun-mode">
<span class="slider round"></span>
</label>
</div>
<div class="form-group row-align">
<div class="label-stack">
<span class="toggle-label">Debug Logs</span>
<span class="toggle-subtext">Enable verbose internal event outputs</span>
</div>
<label class="switch">
<input type="checkbox" id="in-debug">
<span class="slider round"></span>
</label>
</div>
</div>
<div class="actions-container">

View File

@ -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);
}
}
@ -208,7 +294,12 @@ window.addEventListener('DOMContentLoaded', async () => {
btnBack.addEventListener('click', () => switchScreen('home'));
btnSaveConfig.addEventListener('click', handleSaveConfig);
// Check current status on startup (reconnect UI if process already active)
btnImportUrl.addEventListener('click', handleImportUrl);
inImportUrl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleImportUrl();
});
// Check current status on startup
try {
const isAlive = await invoke('get_tunnel_status');
if (isAlive) {

View File

@ -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 {

View File

@ -14,3 +14,4 @@ anyhow = "1.0"
clap = { version = "4.4", features = ["derive"] }
base64 = "0.22"
rand.workspace = true
url = "2.5"

View File

@ -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<String>,
}
fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
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://{}@<YOUR_SERVER_PUBLIC_IP>:50000/?tun=1", key);
println!(" PROXY mode: ostp://{}@<YOUR_SERVER_PUBLIC_IP>: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(())
}