Merge branch 'master' into udc

This commit is contained in:
Mygod
2023-03-07 13:08:16 -05:00
3 changed files with 51 additions and 71 deletions

View File

@@ -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). 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). 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. 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 * Network status monitor mode: This option controls how the app monitors connected devices as well as interface changes
(when custom upstream is used). (when custom upstream is used).
Requires restarting the app to take effects. (best way is to go to app info and force stop) Requires restarting the app to take effects. (best way is to go to app info and force stop)

View File

@@ -9,21 +9,17 @@ import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.util.connectCancellable import be.mygod.vpnhotspot.util.connectCancellable
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.net.HttpCookie import java.math.BigInteger
import java.util.Scanner import java.security.MessageDigest
import java.util.regex.Pattern
/** /**
* This class generates a default nickname for new clients. * This class generates a default nickname for new clients.
@@ -37,43 +33,7 @@ object MacLookup {
override fun getLocalizedMessage() = formatMessage(app) override fun getLocalizedMessage() = formatMessage(app)
} }
private object SessionManager { private val sha1 = MessageDigest.getInstance("SHA-1")
private const val CACHE_FILENAME = "maclookup_sessioncache"
private const val COOKIE_SESSION = "mac_address_vendor_lookup_session"
private val csrfPattern = Pattern.compile("<meta\\s+name=\"csrf-token\"\\s+content=\"([^\"]*)\"",
Pattern.CASE_INSENSITIVE)
private var sessionCache: List<String>?
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<HttpCookie, String> = mutex.withLock {
val sessionCache = (if (forceNew) null else sessionCache) ?: refreshSessionCache()
HttpCookie.parse(sessionCache[0]).single() to sessionCache[1]
}
}
private val macLookupBusy = mutableMapOf<MacAddress, Job>() private val macLookupBusy = mutableMapOf<MacAddress, Job>()
// http://en.wikipedia.org/wiki/ISO_3166-1 // http://en.wikipedia.org/wiki/ISO_3166-1
private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex() private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex()
@@ -84,31 +44,29 @@ object MacLookup {
@MainThread @MainThread
fun perform(mac: MacAddress, explicit: Boolean = false) { fun perform(mac: MacAddress, explicit: Boolean = false) {
abort(mac) abort(mac)
macLookupBusy[mac] = GlobalScope.launch(Dispatchers.IO) { macLookupBusy[mac] = GlobalScope.launch(Dispatchers.Unconfined, CoroutineStart.UNDISPATCHED) {
try {
var response: String? = null var response: String? = null
for (tries in 0 until 5) { try {
val (cookie, csrf) = SessionManager.obtain(tries > 0) response = connectCancellable("https://api.maclookup.app/v2/macs/$mac") { conn ->
response = connectCancellable("https://macaddress.io/mac-address-lookup") { conn -> // conn.setRequestProperty("X-App-Id", "net.mobizme.macaddress")
conn.requestMethod = "POST" // conn.setRequestProperty("X-App-Version", "2.0.11")
conn.setRequestProperty("content-type", "application/json") // conn.setRequestProperty("X-App-Version-Code", "111")
conn.setRequestProperty("cookie", "${cookie.name}=${cookie.value}") val epoch = System.currentTimeMillis()
conn.setRequestProperty("x-csrf-token", csrf) conn.setRequestProperty("X-App-Epoch", epoch.toString())
conn.outputStream.writer().use { it.write("{\"macAddress\":\"$mac\",\"not-web-search\":true}") } conn.setRequestProperty("X-App-Sign", "%032x".format(BigInteger(1,
sha1.digest("aBA6AEkfg8cbHlWrBDYX_${mac}_$epoch".toByteArray()))))
when (val responseCode = conn.responseCode) { when (val responseCode = conn.responseCode) {
200 -> conn.inputStream.bufferedReader().readText() 200 -> conn.inputStream.bufferedReader().readText()
419 -> null 400, 401, 429 -> throw UnexpectedError(mac, conn.inputStream.bufferedReader().readText())
else -> throw IOException("Unhandled response code $responseCode") 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 obj = JSONObject(response)
val result = if (obj.getJSONObject("blockDetails").getBoolean("blockFound")) { if (!obj.getBoolean("success")) throw UnexpectedError(mac, response)
val vendor = obj.getJSONObject("vendorDetails") val result = if (obj.getBoolean("found")) {
val company = vendor.getString("companyName") val company = obj.getString("company")
val match = extractCountry(mac, response, vendor) val match = extractCountry(mac, response, obj)
if (match != null) { if (match != null) {
String(match.groupValues[1].flatMap { listOf('\uD83C', it + 0xDDA5) }.toCharArray()) + ' ' + String(match.groupValues[1].flatMap { listOf('\uD83C', it + 0xDDA5) }.toCharArray()) + ' ' +
company company
@@ -120,15 +78,19 @@ object MacLookup {
} }
} catch (_: CancellationException) { } catch (_: CancellationException) {
} catch (e: Throwable) { } 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() if (explicit) SmartSnackbar.make(e).show()
} }
} }
} }
private fun extractCountry(mac: MacAddress, response: String, obj: JSONObject): MatchResult? { private fun extractCountry(mac: MacAddress, response: String, obj: JSONObject): MatchResult? {
countryCodeRegex.matchEntire(obj.optString("countryCode"))?.also { return it } countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it }
val address = obj.optString("companyAddress") val address = obj.optString("address")
if (address.isBlank()) return null if (address.isBlank()) return null
countryCodeRegex.find(address)?.also { return it } countryCodeRegex.find(address)?.also { return it }
Timber.w(UnexpectedError(mac, response)) Timber.w(UnexpectedError(mac, response))

View File

@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot.net.wifi
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.res.Resources import android.content.res.Resources
import android.net.wifi.SoftApConfiguration import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
@@ -27,11 +28,24 @@ object WifiApManager {
@RequiresApi(30) @RequiresApi(30)
const val RESOURCES_PACKAGE = "com.android.wifi.resources" 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) @get:RequiresApi(30)
val resolvedActivity get() = app.packageManager.queryIntentActivities(Intent(ACTION_RESOURCES_APK), val resolvedActivity: ResolveInfo get() {
PackageManager.MATCH_SYSTEM_ONLY).single() 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" private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported"
val p2pMacRandomizationSupported get() = try { val p2pMacRandomizationSupported get() = try {