From fc58923586940786e80461bf4375b5808ca116f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 08:19:21 +0000 Subject: [PATCH 1/3] fix: captcha manual-pass link not opening and flags not persisting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Captcha URL regex only matched login.vk.com/localhost:PORT, so when the local kernel logs the auth URL on 127.0.0.1:PORT instead, the captcha dialog never triggered. Regex and WebView domain whitelist now accept 127.0.0.1 as well. - Flag switches (VLESS, UDP, DTLS, etc.) only updated in-memory state and were lost on restart unless the user pressed "СОХРАНИТЬ ИЗМЕНЕНИЯ" afterwards. Toggling now saves the config immediately. --- .../vkturn/proxy/ui/CaptchaWebViewDialog.kt | 2 +- .../com/vkturn/proxy/ui/SettingsScreen.kt | 25 +++++++++++++++++-- .../vkturn/proxy/viewmodel/MainViewModel.kt | 3 ++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vkturn/proxy/ui/CaptchaWebViewDialog.kt b/app/src/main/java/com/vkturn/proxy/ui/CaptchaWebViewDialog.kt index 6401d65..b37a53c 100644 --- a/app/src/main/java/com/vkturn/proxy/ui/CaptchaWebViewDialog.kt +++ b/app/src/main/java/com/vkturn/proxy/ui/CaptchaWebViewDialog.kt @@ -55,7 +55,7 @@ fun CaptchaWebViewDialog(captchaUrl: String, onDismiss: () -> Unit) { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { val url = request.url.toString() // Allow all captcha-related domains to load inside WebView - val allowedDomains = listOf("localhost", "fapi.vk.com", "login.vk.com", "api.vk.com", "captcha.vk.com", "vk.com", "vk.ru") + val allowedDomains = listOf("localhost", "127.0.0.1", "fapi.vk.com", "login.vk.com", "api.vk.com", "captcha.vk.com", "vk.com", "vk.ru") val isAllowed = allowedDomains.any { url.contains(it) } // Only dismiss if navigating to a completely unrelated page if (!isAllowed) { diff --git a/app/src/main/java/com/vkturn/proxy/ui/SettingsScreen.kt b/app/src/main/java/com/vkturn/proxy/ui/SettingsScreen.kt index a5d3f13..28bf8ec 100644 --- a/app/src/main/java/com/vkturn/proxy/ui/SettingsScreen.kt +++ b/app/src/main/java/com/vkturn/proxy/ui/SettingsScreen.kt @@ -351,9 +351,20 @@ fun SettingsScreen(viewModel: MainViewModel) { text = { Text("Отключение DTLS с большой вероятностью приведет к блокировке со стороны сервиса. Вы уверены?") }, confirmButton = { TextButton(onClick = { - currentFlags = currentFlags.map { - if (it.argument == "-no-dtls") it.copy(enabled = true) else it + val newFlags = currentFlags.map { + if (it.argument == "-no-dtls") it.copy(enabled = true) else it } + currentFlags = newFlags + viewModel.saveClientConfig(clientConfig.copy( + serverAddress = peer, + vkLink = link, + linkArgument = linkArg, + threads = threads.toInt(), + localPort = listen, + isRawMode = false, + rawCommand = rawCommand, + customFlags = newFlags + )) showDtlsWarning = false }) { Text("Продолжить", color = MaterialTheme.colorScheme.error) @@ -395,6 +406,16 @@ fun SettingsScreen(viewModel: MainViewModel) { } currentFlags = newFlags + viewModel.saveClientConfig(clientConfig.copy( + serverAddress = peer, + vkLink = link, + linkArgument = linkArg, + threads = threads.toInt(), + localPort = listen, + isRawMode = false, + rawCommand = rawCommand, + customFlags = newFlags + )) } }, modifier = Modifier.scale(0.8f) diff --git a/app/src/main/java/com/vkturn/proxy/viewmodel/MainViewModel.kt b/app/src/main/java/com/vkturn/proxy/viewmodel/MainViewModel.kt index aa57929..c99186d 100644 --- a/app/src/main/java/com/vkturn/proxy/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/vkturn/proxy/viewmodel/MainViewModel.kt @@ -93,7 +93,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } private fun checkForCaptcha(log: String) { - val urlPattern = "(https?://(localhost:\\d+|login\\.vk\\.com)\\S*)".toRegex() + // Match URLs on localhost or 127.0.0.1 (local kernel server) as well as login.vk.com + val urlPattern = "(https?://(localhost:\\d+|127\\.0\\.0\\.1:\\d+|login\\.vk\\.com)\\S*)".toRegex() val match = urlPattern.find(log) if (match != null) { _proxyState.value = ProxyState.CaptchaRequired(match.value) From c732c9cbf4cc56ca137d146e4cd67243b4bf9653 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 08:26:57 +0000 Subject: [PATCH 2/3] ci: add GitHub Actions APK build with fixed test signing Adds a workflow that builds and uploads a release APK on every push to main/this branch. Signing uses a static test keystore committed to the repo (app/ci-release.keystore.jks) instead of the default auto-generated debug key, so the signature stays identical across builds/runners and users can update the app without a reinstall. This keystore is for CI/testing only, not Google Play distribution. --- .github/workflows/build-apk.yml | 50 ++++++++++++++++++++++++++++++++ app/build.gradle.kts | 12 ++++++++ app/ci-release.keystore.jks | Bin 0 -> 2752 bytes 3 files changed, 62 insertions(+) create mode 100644 .github/workflows/build-apk.yml create mode 100644 app/ci-release.keystore.jks diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml new file mode 100644 index 0000000..fe22526 --- /dev/null +++ b/.github/workflows/build-apk.yml @@ -0,0 +1,50 @@ +name: Build APK + +on: + push: + branches: + - main + - claude/main-bugs-review-nd8dax + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install required SDK packages + run: sdkmanager "platforms;android-36" "build-tools;36.0.0" + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + gradle-${{ runner.os }}- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build release APK + run: ./gradlew assembleRelease --stacktrace + + - name: Upload APK artifacts + uses: actions/upload-artifact@v4 + with: + name: TurnProxy-apk + path: app/build/outputs/apk/release/*.apk + if-no-files-found: error diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 681c037..26100e3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,6 +23,18 @@ android { } } + // Фиксированный тестовый keystore (не для Google Play): используется во всех + // сборках, включая CI, чтобы подпись APK не менялась от сборки к сборке — + // иначе обновление приложения поверх предыдущей версии ломается. + signingConfigs { + getByName("debug") { + storeFile = file("ci-release.keystore.jks") + storePassword = "vkturnproxy-ci" + keyAlias = "ci-release" + keyPassword = "vkturnproxy-ci" + } + } + // Красивое именование APK файлов applicationVariants.all { val variantName = name diff --git a/app/ci-release.keystore.jks b/app/ci-release.keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..e45a83030411cfa56ed5691c5d47666453491e72 GIT binary patch literal 2752 zcma);c{CJ^8pdbFEXJO7CQF3ujD44NO30FZiL67}Ns(xjHj zQnrLf_8IzQOPF!F-E;5hJNK`9|9H=P&htF)^XKopC@fny2*`rMvIRon@~P)j_c(zt zARf!+0>-jk`X$?NW!qi>RIXi42)A1SLB)4Ek{HbV|FlwlMd*U95?DFC-vA*+C<; zIYuYz;!wAwOddY&%7ie68aF@6Ulsqn!FcM1+X4LmAcE9by^gnhMl%GG37sjb^dO}P^E=%IwDFFb__CE$+?B6UqwHR!8> zy)M<}3fEf4SgIHdba+Je03<(?hj#77JylnH-i%WJs8J6?i{XtaO}B_19T#u_`z_^% z%JqdMPKIS!BQPJ3Px!$1?bOo?QseU(?aB0la*&lDfc|lW~nl5F<44b zf5xF<+ZAno!_W9Q6Y6YD`yRb-b}Kusp@*KubWaZ_qxI>-t!%SLlcon9x0rC;GuQ=V+O~%Gaw$| ztb1rUiaRZ;YdX7>mNC1;UTb{zalj$`g@0W86w$aWDcA!RqEa{e1LEN+lAwaHMh8W+%saadG%?=EB6J-xxmg(7sw&e&kw!7OF|yo z4aM-XYg@<{pe)L!GSZtxF)!=J$o6l`e^gHFDH+J_L0&PN3UWns z|8yAIcd{%H%&h@rBrC9lsa2SF)mmceA>y}i?aNvpmQv%TJ6qyEY&jU2>zw+PE*Pxv2xc}coP%D;8D$5~CG+1CzD{b|CjLDK<*4&5r;-?ZZIcI`40&NjDkFFv72m*F5#J-F`vhGj& zz8ap3XjVOslKhQJIKQf(7(c)d5DZWPL;(T-*M7|vKr|o#CGmGiK$RcN>lhpwAg-#d zqpqr{uBol5sjY^>f*<`3!Jv3782d}Q4FUpwt+>A(;QzvPK<*z7mHniaMPzs@!>CrW z*VAp||G_npp3@PanX3poSG^Mf*(4`p!GJ{IGP1jT`j7gf&=K<@O#FQ4{BD56hJTga zZauvCrCOS}yb81OS@ZgVBcjRA-q^cExT7S^`2=8>NLUw@ zs^AY{&qbeD9+hR+J=9ZQzSM@w_nABMNy$h#&39?f>BP77gZ@+O9a}GR^a6xHT+^u@TGVM5Jq_Acz-_HG5Tzax?Msd7t)ivTqag}&Ao|?~&Zd+B(+_@iq+wJALC-=271E$wQY}vFV&7N*N znzQD7Ryez284H&r@eO}Y>Pw3xT3%g#%Ts~_*E8M!|Pfd@IU#q0P85d<6pNLKu8@PVWn}jwJ6ha^#DX3%T1jcCj=C=G87G#~9`bZ+86&yDP|A3gn*0 z73vT(*0)-p>hRa&)8hHO(=I0)-LiT~dfGjAxtDJxRDR;H=R=&+Gl5)QN^ud?hYS)1 zYyw>F>56SHe3}+~pirb>SrGQUEbm)p%{NF!A#)FoEF3r@NklL%e&$k-et*ufJt+2d zRpYy0+ImT>iZKhgCUqZqd3a!2bRhOFcV_c9cgy>7_oD=2z&wdRCtLLPrxFfN;Ly6V zc~nP#`~Ef%knrchoM?*rl#;P+=iNM3yoYBGZ{l>F^2ZwXQ<=?E!iTZfSSw%~pFVdQ zRODtBtKOG}5U=kN^rBQRLN8&C+F8igOKXK2g-Z0IS69C1l%Q--swmFiUmXYt0f5EPctR*fa^2i1_sJy5XlxHmSr(p>6?%2lG%cua gpTz+4^MW32Weo<0=<12C3WL8y#EAOZ{|iC?1cn~_00000 literal 0 HcmV?d00001 From c58cad2dfcc09a1f9516512a632a4618bd3e8365 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 08:39:52 +0000 Subject: [PATCH 3/3] fix: captcha dialog not triggering on VK's id.vk.ru auth domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real kernel log format is "CAPTCHA_SOLVE|||", and the URL's host is id.vk.ru (VK ID), not login.vk.com — the previous regex only knew about login.vk.com/localhost/127.0.0.1, so the whole line was ignored and the captcha dialog never opened. Also the old \S* capture would have swallowed the trailing "|" into the captured URL since it doesn't stop at "|". Now parses the CAPTCHA_SOLVE-prefixed line directly (stopping at the next "|"), with the domain-list match kept only as a fallback. --- .../com/vkturn/proxy/ui/CaptchaWebViewDialog.kt | 2 +- .../com/vkturn/proxy/viewmodel/MainViewModel.kt | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vkturn/proxy/ui/CaptchaWebViewDialog.kt b/app/src/main/java/com/vkturn/proxy/ui/CaptchaWebViewDialog.kt index b37a53c..1da9126 100644 --- a/app/src/main/java/com/vkturn/proxy/ui/CaptchaWebViewDialog.kt +++ b/app/src/main/java/com/vkturn/proxy/ui/CaptchaWebViewDialog.kt @@ -55,7 +55,7 @@ fun CaptchaWebViewDialog(captchaUrl: String, onDismiss: () -> Unit) { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { val url = request.url.toString() // Allow all captcha-related domains to load inside WebView - val allowedDomains = listOf("localhost", "127.0.0.1", "fapi.vk.com", "login.vk.com", "api.vk.com", "captcha.vk.com", "vk.com", "vk.ru") + val allowedDomains = listOf("localhost", "127.0.0.1", "fapi.vk.com", "login.vk.com", "id.vk.ru", "api.vk.com", "captcha.vk.com", "vk.com", "vk.ru") val isAllowed = allowedDomains.any { url.contains(it) } // Only dismiss if navigating to a completely unrelated page if (!isAllowed) { diff --git a/app/src/main/java/com/vkturn/proxy/viewmodel/MainViewModel.kt b/app/src/main/java/com/vkturn/proxy/viewmodel/MainViewModel.kt index c99186d..b24c150 100644 --- a/app/src/main/java/com/vkturn/proxy/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/vkturn/proxy/viewmodel/MainViewModel.kt @@ -93,11 +93,16 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } private fun checkForCaptcha(log: String) { - // Match URLs on localhost or 127.0.0.1 (local kernel server) as well as login.vk.com - val urlPattern = "(https?://(localhost:\\d+|127\\.0\\.0\\.1:\\d+|login\\.vk\\.com)\\S*)".toRegex() - val match = urlPattern.find(log) - if (match != null) { - _proxyState.value = ProxyState.CaptchaRequired(match.value) + // Kernel emits "CAPTCHA_SOLVE|||" for the manual WebView flow. + // The url itself never contains "|", so stop the capture there instead of + // greedily eating the trailing token with it. + val solveMatch = "CAPTCHA_SOLVE\\|[^|]*\\|(https?://[^|\\s]+)".toRegex().find(log) + // Fallback for plain URLs in the log (localhost/127.0.0.1 local server, + // or VK's own auth domains, which have changed over time: login.vk.com -> id.vk.ru). + val fallbackMatch = "(https?://(localhost:\\d+|127\\.0\\.0\\.1:\\d+|id\\.vk\\.ru|login\\.vk\\.com)\\S*)".toRegex().find(log) + val captchaUrl = solveMatch?.groupValues?.get(1) ?: fallbackMatch?.value + if (captchaUrl != null) { + _proxyState.value = ProxyState.CaptchaRequired(captchaUrl) ProxyService.updateNotification("Требуется авторизация", "Нажмите сюда, чтобы пройти капчу!") }