mirror of https://github.com/ospab/ostp.git
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:
parent
b26863e8e5
commit
067ee758cd
|
|
@ -12,7 +12,7 @@
|
||||||
{
|
{
|
||||||
"title": "OSTP",
|
"title": "OSTP",
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 740,
|
"height": 680,
|
||||||
"resizable": false
|
"resizable": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,50 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<div class="editor-container glass">
|
<!-- Import Area -->
|
||||||
<label for="config-editor">Client JSON Config</label>
|
<div class="import-container glass">
|
||||||
<textarea id="config-editor" spellcheck="false" placeholder="Loading configuration..."></textarea>
|
<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>
|
||||||
|
|
||||||
<div class="actions-container">
|
<div class="actions-container">
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
const { invoke } = window.__TAURI__.core;
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
let appState = 'disconnected'; // 'disconnected', 'connecting', 'connected'
|
let appState = 'disconnected';
|
||||||
let pollInterval = null;
|
let pollInterval = null;
|
||||||
let elapsedSeconds = 0;
|
let elapsedSeconds = 0;
|
||||||
let elapsedTimer = null;
|
let elapsedTimer = null;
|
||||||
|
let rawConfigObj = null; // Cache original config object to preserve extra keys
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const btnConnect = document.getElementById('btn-connect');
|
const btnConnect = document.getElementById('btn-connect');
|
||||||
|
|
@ -19,9 +20,17 @@ const settingsScreen = document.getElementById('settings-screen');
|
||||||
const btnGoSettings = document.getElementById('btn-go-settings');
|
const btnGoSettings = document.getElementById('btn-go-settings');
|
||||||
const btnBack = document.getElementById('btn-back');
|
const btnBack = document.getElementById('btn-back');
|
||||||
const btnSaveConfig = document.getElementById('btn-save-config');
|
const btnSaveConfig = document.getElementById('btn-save-config');
|
||||||
const configEditor = document.getElementById('config-editor');
|
|
||||||
const configToast = document.getElementById('config-toast');
|
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
|
// Utils
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0.0 B';
|
if (bytes === 0) return '0.0 B';
|
||||||
|
|
@ -75,11 +84,9 @@ function setUIState(state) {
|
||||||
statusText.textContent = 'Protected';
|
statusText.textContent = 'Protected';
|
||||||
statusText.classList.add('status-connected');
|
statusText.classList.add('status-connected');
|
||||||
|
|
||||||
// Start poll timer
|
|
||||||
if (!pollInterval) {
|
if (!pollInterval) {
|
||||||
pollInterval = setInterval(fetchMetrics, 1000);
|
pollInterval = setInterval(fetchMetrics, 1000);
|
||||||
}
|
}
|
||||||
// Start uptime timer
|
|
||||||
if (!elapsedTimer) {
|
if (!elapsedTimer) {
|
||||||
elapsedSeconds = 0;
|
elapsedSeconds = 0;
|
||||||
elapsedTimer = setInterval(() => {
|
elapsedTimer = setInterval(() => {
|
||||||
|
|
@ -97,15 +104,15 @@ async function handleToggleConnect() {
|
||||||
try {
|
try {
|
||||||
const success = await invoke('start_tunnel');
|
const success = await invoke('start_tunnel');
|
||||||
if (success) {
|
if (success) {
|
||||||
// The start_tunnel call waits briefly or returns if spawn worked
|
|
||||||
// Backend will periodically check status. Let's monitor it.
|
|
||||||
monitorTunnelState();
|
monitorTunnelState();
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to start tunnel process. Check config.json');
|
alert('Failed to start tunnel process.');
|
||||||
setUIState('disconnected');
|
setUIState('disconnected');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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');
|
setUIState('disconnected');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -119,7 +126,6 @@ async function handleToggleConnect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function monitorTunnelState() {
|
async function monitorTunnelState() {
|
||||||
// Check status for up to 5 seconds to confirm it connects
|
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -134,16 +140,16 @@ async function monitorTunnelState() {
|
||||||
if (attempts < 5 && appState === 'connecting') {
|
if (attempts < 5 && appState === 'connecting') {
|
||||||
setTimeout(check, 1000);
|
setTimeout(check, 1000);
|
||||||
} else if (appState === 'connecting') {
|
} 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');
|
setUIState('disconnected');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
setTimeout(check, 1500); // Delay initial check to give it time to boot
|
setTimeout(check, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMetrics() {
|
async function fetchMetrics() {
|
||||||
try {
|
try {
|
||||||
const stats = await invoke('get_metrics'); // Expected format: { bytes_sent: u64, bytes_recv: u64 }
|
const stats = await invoke('get_metrics');
|
||||||
if (stats) {
|
if (stats) {
|
||||||
metricDown.textContent = formatBytes(stats.bytes_recv);
|
metricDown.textContent = formatBytes(stats.bytes_recv);
|
||||||
metricUp.textContent = formatBytes(stats.bytes_sent);
|
metricUp.textContent = formatBytes(stats.bytes_sent);
|
||||||
|
|
@ -152,7 +158,6 @@ async function fetchMetrics() {
|
||||||
console.error('Failed to fetch metrics', e);
|
console.error('Failed to fetch metrics', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also verify process is still alive
|
|
||||||
try {
|
try {
|
||||||
const isAlive = await invoke('get_tunnel_status');
|
const isAlive = await invoke('get_tunnel_status');
|
||||||
if (!isAlive && appState === 'connected') {
|
if (!isAlive && appState === 'connected') {
|
||||||
|
|
@ -163,7 +168,7 @@ async function fetchMetrics() {
|
||||||
|
|
||||||
function switchScreen(target) {
|
function switchScreen(target) {
|
||||||
if (target === 'settings') {
|
if (target === 'settings') {
|
||||||
loadConfigText();
|
loadConfigIntoFields();
|
||||||
homeScreen.classList.remove('active');
|
homeScreen.classList.remove('active');
|
||||||
settingsScreen.classList.add('active');
|
settingsScreen.classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -172,27 +177,108 @@ function switchScreen(target) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfigText() {
|
// Config Management
|
||||||
configEditor.value = 'Loading configuration...';
|
async function loadConfigIntoFields() {
|
||||||
try {
|
try {
|
||||||
const rawConfig = await invoke('get_config');
|
const rawStr = await invoke('get_config');
|
||||||
configEditor.value = rawConfig;
|
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) {
|
} catch (err) {
|
||||||
configEditor.value = '// Error loading configuration: ' + err;
|
console.error('Error loading config', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveConfig() {
|
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 {
|
try {
|
||||||
const val = configEditor.value;
|
const finalJson = JSON.stringify(rawConfigObj, null, 2);
|
||||||
JSON.parse(val); // Validate JSON format first
|
const success = await invoke('save_config', { jsonContent: finalJson });
|
||||||
const success = await invoke('save_config', { jsonContent: val });
|
|
||||||
if (success) {
|
if (success) {
|
||||||
showToast();
|
showToast();
|
||||||
setTimeout(() => switchScreen('home'), 800);
|
setTimeout(() => switchScreen('home'), 800);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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'));
|
btnGoSettings.addEventListener('click', () => switchScreen('settings'));
|
||||||
btnBack.addEventListener('click', () => switchScreen('home'));
|
btnBack.addEventListener('click', () => switchScreen('home'));
|
||||||
btnSaveConfig.addEventListener('click', handleSaveConfig);
|
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 {
|
try {
|
||||||
const isAlive = await invoke('get_tunnel_status');
|
const isAlive = await invoke('get_tunnel_status');
|
||||||
if (isAlive) {
|
if (isAlive) {
|
||||||
|
|
|
||||||
|
|
@ -395,39 +395,175 @@ h2 {
|
||||||
.editor-container {
|
.editor-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 15px;
|
padding: 20px 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 18px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-container label {
|
.editor-container.scrollable {
|
||||||
font-size: 0.8rem;
|
overflow-y: auto;
|
||||||
color: var(--text-secondary);
|
padding-right: 8px;
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
flex: 1;
|
||||||
width: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 12px;
|
padding: 8px 12px;
|
||||||
color: #e2e8f0;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1.5;
|
color: #fff;
|
||||||
resize: none;
|
|
||||||
outline: none;
|
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);
|
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 {
|
.actions-container {
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@ anyhow = "1.0"
|
||||||
clap = { version = "4.4", features = ["derive"] }
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
url = "2.5"
|
||||||
|
|
|
||||||
158
ostp/src/main.rs
158
ostp/src/main.rs
|
|
@ -26,6 +26,54 @@ struct Args {
|
||||||
/// Number of keys to generate
|
/// Number of keys to generate
|
||||||
#[arg(short = 'c', long, default_value_t = 1)]
|
#[arg(short = 'c', long, default_value_t = 1)]
|
||||||
count: usize,
|
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 {
|
fn generate_secure_key(format_type: &str) -> String {
|
||||||
|
|
@ -159,6 +207,13 @@ async fn run_app() -> Result<()> {
|
||||||
return Ok(());
|
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
|
// Handle explicit configuration initialization
|
||||||
if let Some(ref mode_str) = args.init {
|
if let Some(ref mode_str) = args.init {
|
||||||
let is_server = mode_str == "server";
|
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)?)?;
|
fs::write(&args.config, serde_json::to_string_pretty(&dummy)?)?;
|
||||||
println!("Successfully initialized configuration at {:?}", args.config);
|
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(());
|
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?;
|
ostp_server::run_server(server_cfg.listen, server_cfg.access_keys, outbound, debug).await?;
|
||||||
}
|
}
|
||||||
AppMode::Client(client_cfg) => {
|
AppMode::Client(client_cfg) => {
|
||||||
println!("[OSTP Core] Starting in CLIENT mode connecting to {}", client_cfg.server);
|
run_client_directly(client_cfg).await?;
|
||||||
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?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue