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