From 2ea9c154791378828c0313b56aae26a83480b7cf Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 3 Mar 2023 17:45:47 -0500 Subject: [PATCH 1/3] Swap to a new backend yet again --- .../be/mygod/vpnhotspot/client/MacLookup.kt | 98 ++++++------------- 1 file changed, 30 insertions(+), 68 deletions(-) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/MacLookup.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/MacLookup.kt index 2184ced6..7cc513b6 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/MacLookup.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/MacLookup.kt @@ -9,21 +9,17 @@ import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.util.connectCancellable import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import org.json.JSONException import org.json.JSONObject import timber.log.Timber -import java.io.File -import java.io.FileNotFoundException import java.io.IOException -import java.net.HttpCookie -import java.util.Scanner -import java.util.regex.Pattern +import java.math.BigInteger +import java.security.MessageDigest /** * This class generates a default nickname for new clients. @@ -37,43 +33,7 @@ object MacLookup { override fun getLocalizedMessage() = formatMessage(app) } - private object SessionManager { - private const val CACHE_FILENAME = "maclookup_sessioncache" - private const val COOKIE_SESSION = "mac_address_vendor_lookup_session" - private val csrfPattern = Pattern.compile("? - get() = try { - File(app.deviceStorage.cacheDir, CACHE_FILENAME).readText().split('\n', limit = 2) - } catch (_: FileNotFoundException) { - null - } - set(value) = File(app.deviceStorage.cacheDir, CACHE_FILENAME).run { - if (value != null) writeText(value.joinToString("\n")) else if (!delete()) writeText("") - } - private val mutex = Mutex() - - private suspend fun refreshSessionCache() = connectCancellable("https://macaddress.io/api") { conn -> - val cookies = conn.headerFields["set-cookie"] ?: throw IOException("Missing cookies") - var mavls: HttpCookie? = null - for (header in cookies) for (cookie in HttpCookie.parse(header)) { - if (cookie.name == COOKIE_SESSION) mavls = cookie - } - if (mavls == null) throw IOException("Missing set-cookie $COOKIE_SESSION") - val token = conn.inputStream.use { Scanner(it).findWithinHorizon(csrfPattern, 0) } - ?: throw IOException("Missing csrf-token") - listOf(mavls.toString(), csrfPattern.matcher(token).run { - check(matches()) - group(1)!! - }).also { sessionCache = it } - } - - suspend fun obtain(forceNew: Boolean): Pair = mutex.withLock { - val sessionCache = (if (forceNew) null else sessionCache) ?: refreshSessionCache() - HttpCookie.parse(sessionCache[0]).single() to sessionCache[1] - } - } - + private val sha1 = MessageDigest.getInstance("SHA-1") private val macLookupBusy = mutableMapOf() // http://en.wikipedia.org/wiki/ISO_3166-1 private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex() @@ -84,31 +44,29 @@ object MacLookup { @MainThread fun perform(mac: MacAddress, explicit: Boolean = false) { abort(mac) - macLookupBusy[mac] = GlobalScope.launch(Dispatchers.IO) { + macLookupBusy[mac] = GlobalScope.launch(Dispatchers.Unconfined, CoroutineStart.UNDISPATCHED) { + var response: String? = null try { - var response: String? = null - for (tries in 0 until 5) { - val (cookie, csrf) = SessionManager.obtain(tries > 0) - response = connectCancellable("https://macaddress.io/mac-address-lookup") { conn -> - conn.requestMethod = "POST" - conn.setRequestProperty("content-type", "application/json") - conn.setRequestProperty("cookie", "${cookie.name}=${cookie.value}") - conn.setRequestProperty("x-csrf-token", csrf) - conn.outputStream.writer().use { it.write("{\"macAddress\":\"$mac\",\"not-web-search\":true}") } - when (val responseCode = conn.responseCode) { - 200 -> conn.inputStream.bufferedReader().readText() - 419 -> null - else -> throw IOException("Unhandled response code $responseCode") - } + response = connectCancellable("https://api.maclookup.app/v2/macs/$mac") { conn -> +// conn.setRequestProperty("X-App-Id", "net.mobizme.macaddress") +// conn.setRequestProperty("X-App-Version", "2.0.11") +// conn.setRequestProperty("X-App-Version-Code", "111") + val epoch = System.currentTimeMillis() + conn.setRequestProperty("X-App-Epoch", epoch.toString()) + conn.setRequestProperty("X-App-Sign", "%032x".format(BigInteger(1, + sha1.digest("aBA6AEkfg8cbHlWrBDYX_${mac}_$epoch".toByteArray())))) + when (val responseCode = conn.responseCode) { + 200 -> conn.inputStream.bufferedReader().readText() + 400, 401, 429 -> throw UnexpectedError(mac, conn.inputStream.bufferedReader().readText()) + else -> throw UnexpectedError(mac, "Unhandled response code $responseCode: " + + conn.inputStream.bufferedReader().readText()) } - if (response != null) break } - if (response == null) throw IOException("Session creation failure") val obj = JSONObject(response) - val result = if (obj.getJSONObject("blockDetails").getBoolean("blockFound")) { - val vendor = obj.getJSONObject("vendorDetails") - val company = vendor.getString("companyName") - val match = extractCountry(mac, response, vendor) + if (!obj.getBoolean("success")) throw UnexpectedError(mac, response) + val result = if (obj.getBoolean("found")) { + val company = obj.getString("company") + val match = extractCountry(mac, response, obj) if (match != null) { String(match.groupValues[1].flatMap { listOf('\uD83C', it + 0xDDA5) }.toCharArray()) + ' ' + company @@ -120,15 +78,19 @@ object MacLookup { } } catch (_: CancellationException) { } catch (e: Throwable) { - Timber.w(e) + when (e) { + is UnexpectedError -> Timber.w(e) + is IOException -> Timber.d(e) + else -> Timber.w(UnexpectedError(mac, "Got response: $response").initCause(e)) + } if (explicit) SmartSnackbar.make(e).show() } } } private fun extractCountry(mac: MacAddress, response: String, obj: JSONObject): MatchResult? { - countryCodeRegex.matchEntire(obj.optString("countryCode"))?.also { return it } - val address = obj.optString("companyAddress") + countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it } + val address = obj.optString("address") if (address.isBlank()) return null countryCodeRegex.find(address)?.also { return it } Timber.w(UnexpectedError(mac, response)) From 08fc8df24b12831308c4d7df62ebd570c7b1e9b8 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 4 Mar 2023 11:14:14 -0500 Subject: [PATCH 2/3] Refine resolving action resources apk --- .../vpnhotspot/net/wifi/WifiApManager.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt index f65c94b4..0a352762 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt @@ -3,6 +3,7 @@ package be.mygod.vpnhotspot.net.wifi import android.annotation.TargetApi import android.content.Intent import android.content.pm.PackageManager +import android.content.pm.ResolveInfo import android.content.res.Resources import android.net.wifi.SoftApConfiguration import android.net.wifi.WifiManager @@ -27,11 +28,24 @@ object WifiApManager { @RequiresApi(30) const val RESOURCES_PACKAGE = "com.android.wifi.resources" /** - * Based on: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/000ad45/service/java/com/android/server/wifi/WifiContext.java#66 + * Based on: https://cs.android.com/android/platform/superproject/+/master:packages/modules/Wifi/framework/java/android/net/wifi/WifiContext.java;l=66;drc=5ca657189aac546af0aafaba11bbc9c5d889eab3 */ @get:RequiresApi(30) - val resolvedActivity get() = app.packageManager.queryIntentActivities(Intent(ACTION_RESOURCES_APK), - PackageManager.MATCH_SYSTEM_ONLY).single() + val resolvedActivity: ResolveInfo get() { + val list = app.packageManager.queryIntentActivities(Intent(ACTION_RESOURCES_APK), + PackageManager.MATCH_SYSTEM_ONLY) + require(list.isNotEmpty()) { "Missing $ACTION_RESOURCES_APK" } + if (list.size > 1) { + list.singleOrNull { + it.activityInfo.applicationInfo.sourceDir.startsWith("/apex/com.android.wifi") + }?.let { return it } + Timber.w(Exception("Found > 1 apk: " + list.joinToString { + val info = it.activityInfo.applicationInfo + "${info.packageName} (${info.sourceDir})" + })) + } + return list[0] + } private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported" val p2pMacRandomizationSupported get() = try { From 66be6aebf58de68675e5c0ea9f5b1c32e8e4b075 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 4 Mar 2023 12:08:45 -0500 Subject: [PATCH 3/3] Add doc for Use system configuration for temporary hotspot --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 07e1c128..e00da41a 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,10 @@ Default settings are picked to suit general use cases and maximize compatibility You might be required to turn this mode off if you want to use short SSID (at most 8 bytes long). Unsafe mode might not work for your device, and there is a small chance you will soft brick your device (recoverable). See [#153](https://github.com/Mygod/VPNHotspot/issues/153) for more information. +* Use system configuration for temporary hotspot: (Android 11 or newer) + Attempt to start a temporary hotspot using system Wi-Fi hotspot configuration. + This feature is most likely only functional on Android 12 or newer. + Enabling this switch will also prevent other apps from using the [local-only hotspot](https://developer.android.com/guide/topics/connectivity/localonlyhotspot) functionality. * Network status monitor mode: This option controls how the app monitors connected devices as well as interface changes (when custom upstream is used). Requires restarting the app to take effects. (best way is to go to app info and force stop)