diff --git a/ostp-gui/src-tauri/Cargo.lock b/ostp-gui/src-tauri/Cargo.lock index eb91ebb..7820164 100644 --- a/ostp-gui/src-tauri/Cargo.lock +++ b/ostp-gui/src-tauri/Cargo.lock @@ -2665,16 +2665,14 @@ dependencies = [ [[package]] name = "ostp-client" -version = "0.2.97" +version = "0.4.0" dependencies = [ "anyhow", "base64 0.22.1", "bytes", - "chacha20poly1305", "chrono", "futures", "futures-util", - "hex", "hmac", "json_comments", "libc", @@ -2694,12 +2692,11 @@ dependencies = [ "tun", "webpki-roots 0.26.11", "winapi", - "x25519-dalek", ] [[package]] name = "ostp-core" -version = "0.2.97" +version = "0.4.0" dependencies = [ "anyhow", "bytes", @@ -2722,6 +2719,7 @@ dependencies = [ "json_comments", "ostp-client", "portable-atomic", + "qrcode", "rand", "serde", "serde_json", @@ -2734,7 +2732,7 @@ dependencies = [ [[package]] name = "ostp-tun" -version = "0.2.97" +version = "0.4.0" dependencies = [ "anyhow", "libc", @@ -3092,6 +3090,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" + [[package]] name = "quick-xml" version = "0.39.4" diff --git a/ostp-gui/src-tauri/Cargo.toml b/ostp-gui/src-tauri/Cargo.toml index 99485a1..3e27815 100644 --- a/ostp-gui/src-tauri/Cargo.toml +++ b/ostp-gui/src-tauri/Cargo.toml @@ -29,4 +29,5 @@ ostp-client = { path = "../../ostp-client" } portable-atomic = "1" json_comments = "0.2" rand = "0.8" +qrcode = { version = "0.14", default-features = false, features = ["svg"] } diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index 2a5a70c..51f64ad 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -483,6 +483,20 @@ async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result Ok(true) } +/// Render a share link to an SVG QR code locally. The access key never leaves +/// the device — unlike an online QR service. (Ported from the current ostp-gui.) +#[tauri::command] +fn generate_qr(text: String) -> Result { + let code = qrcode::QrCode::new(text.as_bytes()).map_err(|e| e.to_string())?; + let svg = code + .render::() + .min_dimensions(220, 220) + .dark_color(qrcode::render::svg::Color("#000000")) + .light_color(qrcode::render::svg::Color("#ffffff")) + .build(); + Ok(svg) +} + #[tauri::command] async fn start_tunnel(state: tauri::State<'_, AppState>, app: tauri::AppHandle) -> Result { let mut guard = state.0.lock().await; @@ -859,7 +873,7 @@ pub fn run() { } _ => {} }) - .invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, get_wintun_install_path, set_autostart, get_autostart, list_running_processes]) + .invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, get_wintun_install_path, set_autostart, get_autostart, list_running_processes, generate_qr]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/ostp-gui/src/i18n.js b/ostp-gui/src/i18n.js index 63653e4..5fd1ef7 100644 --- a/ostp-gui/src/i18n.js +++ b/ostp-gui/src/i18n.js @@ -54,6 +54,11 @@ const translations = { label_launch_startup: 'Launch at Startup', launch_startup_hint: 'Start OSTP with Windows', cancel_btn: 'Cancel', + share_btn: 'Share', + share_title: 'Share configuration', + share_desc: 'Scan the QR or copy the link. The QR is generated locally — the access key never leaves this device.', + copy_btn: 'Copy link', + close_btn: 'Close', wintun_missing_title: 'Wintun Driver Missing', wintun_missing_desc: 'TUN mode requires the Wintun network driver (wintun.dll).', wintun_step1: 'Download wintun.zip from the official site', @@ -114,6 +119,11 @@ const translations = { label_launch_startup: 'Запуск вместе с Windows', launch_startup_hint: 'Автозапуск OSTP при входе в систему', cancel_btn: 'Отмена', + share_btn: 'Поделиться', + share_title: 'Поделиться конфигурацией', + share_desc: 'Отсканируйте QR или скопируйте ссылку. QR генерируется локально — ключ доступа не покидает устройство.', + copy_btn: 'Копировать ссылку', + close_btn: 'Закрыть', wintun_missing_title: 'Отсутствует драйвер Wintun', wintun_missing_desc: 'Режим TUN требует сетевой драйвер Wintun (wintun.dll).', wintun_step1: 'Скачайте wintun.zip с официального сайта', diff --git a/ostp-gui/src/index.html b/ostp-gui/src/index.html index 5a6339b..42f80e6 100644 --- a/ostp-gui/src/index.html +++ b/ostp-gui/src/index.html @@ -165,6 +165,7 @@ data-i18n-placeholder="import_placeholder" placeholder="Paste ostp:// share link..." /> + @@ -366,6 +367,20 @@ + + + diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index 5c8dc9d..f60ab4e 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -36,8 +36,14 @@ const btnGoSettings = $('btn-go-settings'); const btnAutoConnect = $('btn-auto-connect'); const btnBack = $('btn-back'); const btnImport = $('btn-import-url'); +const btnShare = $('btn-share-url'); const btnPeekKey = $('btn-peek-key'); const importInput = $('in-import-url'); +const shareModal = $('share-modal'); +const shareQr = $('share-qr'); +const shareLink = $('share-link'); +const btnShareClose = $('btn-share-close'); +const btnShareCopy = $('btn-share-copy'); const inServer = $('in-server'); const inKey = $('in-key'); const inSocks = $('in-socks'); @@ -463,6 +469,44 @@ function handleImport() { } } +// ── Share config (QR + ostp:// link) ────────────────────────────────────────── +function buildShareLink() { + const host = inServer.value.trim(); + const key = inKey.value.trim(); + if (!host || !key) return ''; + const params = []; + const sni = inSni.value.trim(); + if (sni) params.push(`sni=${encodeURIComponent(sni)}`); + if (inTransport.value && inTransport.value !== 'udp') params.push(`type=${inTransport.value}`); + const qs = params.length ? `?${params.join('&')}` : ''; + return `ostp://${encodeURIComponent(key)}@${host}${qs}`; +} + +async function handleShare() { + const link = buildShareLink(); + if (!link) { showToast('Set server and key first', 'error'); return; } + shareLink.value = link; + shareQr.innerHTML = ''; + try { + const svg = await invoke('generate_qr', { text: link }); + if (svg) shareQr.innerHTML = svg; + } catch (err) { + console.error('generate_qr failed', err); + } + shareModal.classList.remove('hidden'); +} + +async function copyShareLink() { + try { + await navigator.clipboard.writeText(shareLink.value); + showToast('Link copied', 'ok'); + } catch { + shareLink.select(); + document.execCommand('copy'); + showToast('Link copied', 'ok'); + } +} + // ── Peek key ────────────────────────────────────────────────────────────────── let peeking = false; function togglePeek() { @@ -578,6 +622,10 @@ window.addEventListener('DOMContentLoaded', async () => { btnGoSettings.addEventListener('click', () => showScreen('settings')); btnBack.addEventListener('click', () => showScreen('home')); btnImport.addEventListener('click', handleImport); + if (btnShare) btnShare.addEventListener('click', handleShare); + if (btnShareClose) btnShareClose.addEventListener('click', () => shareModal.classList.add('hidden')); + if (btnShareCopy) btnShareCopy.addEventListener('click', copyShareLink); + if (shareModal) shareModal.addEventListener('click', e => { if (e.target === shareModal) shareModal.classList.add('hidden'); }); btnPeekKey.addEventListener('click', togglePeek); // Theme toggle diff --git a/ostp-gui/src/styles.css b/ostp-gui/src/styles.css index 4d1cf5d..4160e0f 100644 --- a/ostp-gui/src/styles.css +++ b/ostp-gui/src/styles.css @@ -263,6 +263,25 @@ html[data-theme="light"] .watermark { filter: invert(1); } +/* ── Share modal (QR) ─────────────────────────────────────────────────────── */ +.share-qr { + display: flex; + justify-content: center; + margin: 8px 0 14px; +} +.share-qr svg { + width: 220px; + height: 220px; + background: #fff; + border-radius: var(--r-sm); + padding: 10px; +} +#share-link { + margin-bottom: 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; +} + /* ── Screen system ────────────────────────────────────────────────────────── */ .screen { position: absolute;