mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-19 08:04:39 +03:00
fix: Android full tunnel mode requires credentials + deployment IDs UI refactor (#124)
Real bug I introduced in #94: Full mode was skipping the credential check that apps_script mode enforces, but Full mode does talk to CodeFull.gs on Apps Script and needs the same auth_key + deployment ID. Users flipping to Full mode with empty fields would silently fail. Two sites fixed: - MhrvVpnService.kt — changed `mode == APPS_SCRIPT` gate to `mode != GOOGLE_ONLY` - HomeScreen.kt — removed the `cfg.mode == Mode.FULL` bypass in the Start button's enabled-state Also includes a UX improvement for the Deployment IDs editor (per-row field with add/remove buttons instead of raw newline-separated text), which makes multi-deployment setups easier to manage on Android. Rust-side 75 tests still green, Kotlin compiles clean. Android-only diff so no Rust CI impact.
This commit is contained in:
@@ -93,11 +93,13 @@ class MhrvVpnService : VpnService() {
|
||||
// the instant I tap Start". See issue #73.
|
||||
startForeground(NOTIF_ID, buildNotif(cfg.listenPort))
|
||||
|
||||
// Deployment ID + auth key are only required in apps_script mode.
|
||||
// google_only (bootstrap) and full (tunnel) modes run without them.
|
||||
val needsAppsScriptCreds = cfg.mode == Mode.APPS_SCRIPT
|
||||
if (needsAppsScriptCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
|
||||
Log.e(TAG, "Config is incomplete — can't start proxy in apps_script mode")
|
||||
// Deployment ID + auth key are required for apps_script and full
|
||||
// modes — both talk to Apps Script. Only google_only (bootstrap)
|
||||
// runs without them. Closes #73 regression where google_only
|
||||
// users hit this branch and crashed on startForeground timeout.
|
||||
val needsCreds = cfg.mode != Mode.GOOGLE_ONLY
|
||||
if (needsCreds && (!cfg.hasDeploymentId || cfg.authKey.isBlank())) {
|
||||
Log.e(TAG, "Config is incomplete — deployment ID + auth key required for ${cfg.mode}")
|
||||
try { stopForeground(STOP_FOREGROUND_REMOVE) } catch (_: Throwable) {}
|
||||
stopSelf()
|
||||
return
|
||||
|
||||
@@ -418,7 +418,6 @@ fun HomeScreen(
|
||||
},
|
||||
enabled = (isVpnRunning ||
|
||||
cfg.mode == Mode.GOOGLE_ONLY ||
|
||||
cfg.mode == Mode.FULL ||
|
||||
(cfg.hasDeploymentId && cfg.authKey.isNotBlank())) && !transitionCooldown,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isVpnRunning) ErrRed else OkGreen,
|
||||
@@ -689,7 +688,7 @@ private fun ConnectionModeDropdown(
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Deployment IDs editor (multi-line, one URL/ID per line).
|
||||
// Deployment IDs editor — one row per ID, with add/remove buttons.
|
||||
// =========================================================================
|
||||
|
||||
@Composable
|
||||
@@ -698,26 +697,79 @@ private fun DeploymentIdsField(
|
||||
onChange: (List<String>) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
// Treat the list as newline-joined text. Keep trailing newlines so the
|
||||
// cursor behaves naturally while the user is adding a new entry.
|
||||
var raw by remember(urls) { mutableStateOf(urls.joinToString("\n")) }
|
||||
var newEntry by remember { mutableStateOf("") }
|
||||
|
||||
OutlinedTextField(
|
||||
value = raw,
|
||||
onValueChange = {
|
||||
raw = it
|
||||
val parsed = it.split("\n").map(String::trim).filter(String::isNotBlank)
|
||||
onChange(parsed)
|
||||
},
|
||||
label = { Text(stringResource(R.string.field_deployment_urls)) },
|
||||
enabled = enabled,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 2,
|
||||
maxLines = 6,
|
||||
supportingText = {
|
||||
Text(stringResource(R.string.help_deployment_urls))
|
||||
},
|
||||
)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.field_deployment_urls),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
|
||||
// Existing entries — each with its own row and a remove button.
|
||||
urls.forEachIndexed { index, url ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = url,
|
||||
onValueChange = { edited ->
|
||||
val updated = urls.toMutableList()
|
||||
updated[index] = edited
|
||||
onChange(updated)
|
||||
},
|
||||
enabled = enabled,
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodySmall,
|
||||
label = { Text("#${index + 1}") },
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
onChange(urls.filterIndexed { i, _ -> i != index })
|
||||
},
|
||||
enabled = enabled,
|
||||
) {
|
||||
Text("✕", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "Add" row: text field + button.
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newEntry,
|
||||
onValueChange = { newEntry = it },
|
||||
enabled = enabled,
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
placeholder = { Text("Paste URL or ID") },
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
val trimmed = newEntry.trim()
|
||||
if (trimmed.isNotBlank()) {
|
||||
onChange(urls + trimmed)
|
||||
newEntry = ""
|
||||
}
|
||||
},
|
||||
enabled = enabled && newEntry.isNotBlank(),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
) {
|
||||
Text("+ Add")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.help_deployment_urls),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<string name="lang_toggle_cd">تغییر زبان</string>
|
||||
|
||||
<!-- Supporting / helper text -->
|
||||
<string name="help_deployment_urls">یکی در هر خط. میتوانید URL کامل (https://script.google.com/macros/s/.../exec) یا فقط ID خام بگذارید — ترکیبی هم قبول است. چند ID بهصورت چرخشی (round-robin) استفاده میشوند.</string>
|
||||
<string name="help_deployment_urls">URL کامل (https://script.google.com/macros/s/.../exec) یا فقط ID خام. چند ID بهصورت چرخشی استفاده میشوند — بیشتر ID = سرعت بیشتر در حالت تونل کامل.</string>
|
||||
<string name="help_auth_key">همان رمز مشترکی که داخل Apps Script گذاشتید.</string>
|
||||
<string name="help_mode_vpn_tun">هنگام اتصال، مجوز VPN سیستم درخواست میشود. تمام ترافیک دستگاه بهصورت خودکار رد میشود.</string>
|
||||
<string name="help_mode_proxy_only">بدون VPN سیستم. بعد از اتصال، پروکسی Wi-Fi را روی 127.0.0.1:%1$d (HTTP) یا %2$d (SOCKS5) تنظیم کنید. فقط برنامههایی که تنظیمات پروکسی را رعایت میکنند رد میشوند.</string>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<string name="lang_toggle_cd">Switch language</string>
|
||||
|
||||
<!-- Supporting / helper text -->
|
||||
<string name="help_deployment_urls">One per line. Full URLs (https://script.google.com/macros/s/.../exec) or bare IDs — mix as you like. Multiple IDs are rotated round-robin.</string>
|
||||
<string name="help_deployment_urls">Full URLs (https://script.google.com/macros/s/.../exec) or bare IDs. Multiple IDs are rotated round-robin — more IDs = more pipeline throughput in full mode.</string>
|
||||
<string name="help_auth_key">The shared secret you set in the Apps Script.</string>
|
||||
<string name="help_mode_vpn_tun">Requests the OS VPN grant on Connect. All device traffic is routed automatically.</string>
|
||||
<string name="help_mode_proxy_only">No OS VPN. Set your Wi-Fi proxy to 127.0.0.1:%1$d (HTTP) or %2$d (SOCKS5) after Connect. Only apps that honour the proxy settings will tunnel.</string>
|
||||
|
||||
Reference in New Issue
Block a user