Merge branch 'master' into udc
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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("<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 sha1 = MessageDigest.getInstance("SHA-1")
|
||||
private val macLookupBusy = mutableMapOf<MacAddress, Job>()
|
||||
// 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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user