handle mib session expirey and keepalive

This commit is contained in:
2026-05-15 10:24:51 +05:00
parent c9ec43de04
commit 1b5a417196
3 changed files with 89 additions and 6 deletions
@@ -9,6 +9,8 @@ import java.security.MessageDigest
import java.util.concurrent.TimeUnit
import kotlin.random.Random
class SessionExpiredException : Exception("MIB session expired")
class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
private val TAG = "MibLoginFlow"
@@ -22,6 +24,15 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
var lastProfiles: List<MibProfile> = emptyList()
private set
/** Called after automatic session recovery so callers can update their session reference. */
var onSessionRefreshed: ((MibSession, List<MibProfile>) -> Unit)? = null
// Stored after login so the session can be silently recovered on 419
@Volatile private var storedUsername: String? = null
@Volatile private var storedPasswordHash: String? = null
@Volatile private var storedOtpSeed: String? = null
private var inRelogin = false
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
@@ -46,6 +57,9 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
* Returns list of accounts from all profiles on success.
*/
fun login(username: String, passwordHash: String, otpSeed: String): List<MibAccount> {
storedUsername = username
storedPasswordHash = passwordHash
storedOtpSeed = otpSeed
val appId = getOrCreateAppId()
val key1 = prefs.getString("mib_key1_$username", null)
val key2 = prefs.getString("mib_key2_$username", null)
@@ -172,17 +186,48 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
}
private fun doRequest(session: MibSession, data: JSONObject, sfunc: String): JSONObject {
val routePath = data.optString("routePath", "?")
val response = try {
sendRequest(session, data, sfunc)
} catch (e: SessionExpiredException) {
if (inRelogin) throw e
"" // fall through to recovery below
}
// Detect expired session: HTTP 419 (empty response) or JSON with reasonCode 505
val isExpired = response.isEmpty() ||
(response.trimStart().startsWith("{") &&
JSONObject(response).optString("reasonCode") == "505")
if (isExpired && !inRelogin) {
val u = storedUsername ?: throw SessionExpiredException()
val ph = storedPasswordHash ?: throw SessionExpiredException()
val os = storedOtpSeed ?: throw SessionExpiredException()
inRelogin = true
try { login(u, ph, os) } finally { inRelogin = false }
val newSession = lastSession ?: throw SessionExpiredException()
onSessionRefreshed?.invoke(newSession, lastProfiles)
// Refresh nonce/xxid in the payload for the retry
data.put("nonce", MibNonce.generate(newSession.nonceGenerator))
data.put("appId", newSession.appId)
data.put("sodium", MibNonce.randomSodium())
data.put("xxid", newSession.xxid)
val retryResponse = sendRequest(newSession, data, sfunc)
if (retryResponse.trimStart().startsWith("{")) return JSONObject(retryResponse)
return MibCrypto.decrypt(retryResponse, newSession.sessionKey)
}
if (response.trimStart().startsWith("{")) return JSONObject(response)
return MibCrypto.decrypt(response, session.sessionKey)
}
private fun sendRequest(session: MibSession, data: JSONObject, sfunc: String): String {
val encrypted = MibCrypto.encrypt(data, session.sessionKey)
val formBody = FormBody.Builder()
.add("xxid", session.xxid)
.add("sfunc", sfunc)
.add("data", encrypted)
.build()
val response = post(formBody)
// Server returns plain JSON (not encrypted) for error responses (e.g. expired session)
if (response.trimStart().startsWith("{")) return JSONObject(response)
return MibCrypto.decrypt(response, session.sessionKey)
return post(formBody)
}
private fun baseData(session: MibSession, routePath: String): JSONObject = JSONObject().apply {
@@ -276,6 +321,7 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
.post(body)
.build()
val response = client.newCall(request).execute()
if (response.code == 419) throw SessionExpiredException()
return response.body?.string() ?: throw IllegalStateException("Empty response body")
}