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