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)
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))
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 {