Swap to a new backend yet again

This commit is contained in:
Mygod
2023-03-03 17:45:47 -05:00
parent 72e23bc886
commit 2ea9c15479

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) {
var response: String? = null
try { try {
var response: String? = null response = connectCancellable("https://api.maclookup.app/v2/macs/$mac") { conn ->
for (tries in 0 until 5) { // conn.setRequestProperty("X-App-Id", "net.mobizme.macaddress")
val (cookie, csrf) = SessionManager.obtain(tries > 0) // conn.setRequestProperty("X-App-Version", "2.0.11")
response = connectCancellable("https://macaddress.io/mac-address-lookup") { conn -> // conn.setRequestProperty("X-App-Version-Code", "111")
conn.requestMethod = "POST" val epoch = System.currentTimeMillis()
conn.setRequestProperty("content-type", "application/json") conn.setRequestProperty("X-App-Epoch", epoch.toString())
conn.setRequestProperty("cookie", "${cookie.name}=${cookie.value}") conn.setRequestProperty("X-App-Sign", "%032x".format(BigInteger(1,
conn.setRequestProperty("x-csrf-token", csrf) sha1.digest("aBA6AEkfg8cbHlWrBDYX_${mac}_$epoch".toByteArray()))))
conn.outputStream.writer().use { it.write("{\"macAddress\":\"$mac\",\"not-web-search\":true}") } when (val responseCode = conn.responseCode) {
when (val responseCode = conn.responseCode) { 200 -> conn.inputStream.bufferedReader().readText()
200 -> conn.inputStream.bufferedReader().readText() 400, 401, 429 -> throw UnexpectedError(mac, conn.inputStream.bufferedReader().readText())
419 -> null else -> throw UnexpectedError(mac, "Unhandled response code $responseCode: " +
else -> throw IOException("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))