diff --git a/app/build.gradle b/app/build.gradle index cf1bc55e..710aec8f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,6 +58,7 @@ android { dataBinding = true } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility '1.8' targetCompatibility '1.8' } @@ -105,6 +106,8 @@ tasks.whenTaskAdded { task -> dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' + kapt "androidx.room:room-compiler:${versions.room}" debugImplementation "com.amitshekhar.android:debug-db:1.0.5" @@ -181,6 +184,7 @@ dependencies { //implementation "org.redundent:kotlin-xml-builder:1.5.3" implementation "io.github.wulkanowy:signer-android:0.1.1" + implementation 'com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31' implementation "androidx.work:work-runtime-ktx:${versions.work}" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 9a2f84d6..88bdc603 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -294,6 +294,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { "Vulcan" ) + val pushVulcanHebeApp = FirebaseApp.initializeApp( + this@App, + FirebaseOptions.Builder() + .setProjectId("dzienniczekplus") + .setStorageBucket("dzienniczekplus.appspot.com") + .setDatabaseUrl("https://dzienniczekplus.firebaseio.com") + .setGcmSenderId("987828170337") + .setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA") + .setApplicationId("1:987828170337:android:7e16404b9e5deaaa") + .build(), + "VulcanHebe" + ) + try { FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener { instanceIdResult -> val token = instanceIdResult.token @@ -324,6 +337,14 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { config.sync.tokenVulcanList = listOf() } } + FirebaseInstanceId.getInstance(pushVulcanHebeApp).instanceId.addOnSuccessListener { instanceIdResult -> + val token = instanceIdResult.token + d("Firebase", "Got VulcanHebe token: $token") + if (token != config.sync.tokenVulcanHebe) { + config.sync.tokenVulcanHebe = token + config.sync.tokenVulcanHebeList = listOf() + } + } FirebaseMessaging.getInstance().subscribeToTopic(packageName) } catch (e: IllegalStateException) { e.printStackTrace() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt index 2d7dcf78..068400a1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt @@ -99,6 +99,10 @@ class ConfigSync(private val config: Config) { var tokenVulcan: String? get() { mTokenVulcan = mTokenVulcan ?: config.values.get("tokenVulcan", null as String?); return mTokenVulcan } set(value) { config.set("tokenVulcan", value); mTokenVulcan = value } + private var mTokenVulcanHebe: String? = null + var tokenVulcanHebe: String? + get() { mTokenVulcanHebe = mTokenVulcanHebe ?: config.values.get("tokenVulcanHebe", null as String?); return mTokenVulcanHebe } + set(value) { config.set("tokenVulcanHebe", value); mTokenVulcanHebe = value } private var mTokenMobidziennikList: List? = null var tokenMobidziennikList: List @@ -112,6 +116,10 @@ class ConfigSync(private val config: Config) { var tokenVulcanList: List get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() } set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value } + private var mTokenVulcanHebeList: List? = null + var tokenVulcanHebeList: List + get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() } + set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value } private var mRegisterAvailability: Map? = null var registerAvailability: Map diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index 5bc08159..016d0eb4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -95,7 +95,16 @@ const val VULCAN_API_APP_NAME = "VULCAN-Android-ModulUcznia" const val VULCAN_API_APP_VERSION = "20.5.1.470" const val VULCAN_API_PASSWORD = "CE75EA598C7743AD9B0B7328DED85B06" const val VULCAN_API_PASSWORD_FAKELOG = "012345678901234567890123456789AB" -val VULCAN_API_DEVICE_NAME = "Szkolny.eu ${Build.MODEL}" +const val VULCAN_HEBE_USER_AGENT = "Dart/2.10 (dart:io)" +const val VULCAN_HEBE_APP_NAME = "DzienniczekPlus 2.0" +const val VULCAN_HEBE_APP_VERSION = "21.02.09 (G)" +private const val VULCAN_API_DEVICE_NAME_PREFIX = "Szkolny.eu " +private const val VULCAN_API_DEVICE_NAME_SUFFIX = " - nie usuwać" +val VULCAN_API_DEVICE_NAME by lazy { + val base = "$VULCAN_API_DEVICE_NAME_PREFIX${Build.MODEL}" + val baseMaxLength = 50 - VULCAN_API_DEVICE_NAME_SUFFIX.length + base.take(baseMaxLength) + VULCAN_API_DEVICE_NAME_SUFFIX +} const val VULCAN_API_ENDPOINT_CERTIFICATE = "mobile-api/Uczen.v3.UczenStart/Certyfikat" const val VULCAN_API_ENDPOINT_STUDENT_LIST = "mobile-api/Uczen.v3.UczenStart/ListaUczniow" @@ -116,6 +125,8 @@ const val VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/ const val VULCAN_API_ENDPOINT_HOMEWORK_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/ZadaniaDomoweZalacznik" const val VULCAN_WEB_ENDPOINT_LUCKY_NUMBER = "Start.mvc/GetKidsLuckyNumbers" const val VULCAN_WEB_ENDPOINT_REGISTER_DEVICE = "RejestracjaUrzadzeniaToken.mvc/Get" +const val VULCAN_HEBE_ENDPOINT_REGISTER_NEW = "api/mobile/register/new" +const val VULCAN_HEBE_ENDPOINT_MAIN = "api/mobile/register/hebe" const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt index 8301f62f..c7a47e24 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt @@ -170,6 +170,7 @@ const val ERROR_VULCAN_WEB_LOGGED_OUT = 350 const val ERROR_VULCAN_WEB_CERTIFICATE_POST_FAILED = 351 const val ERROR_VULCAN_WEB_GRADUATE_ACCOUNT = 352 const val ERROR_VULCAN_WEB_NO_SCHOOLS = 353 +const val ERROR_VULCAN_HEBE_OTHER = 354 const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401 const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402 @@ -229,5 +230,6 @@ const val ERROR_ONEDRIVE_DOWNLOAD = 930 const val EXCEPTION_VULCAN_WEB_LOGIN = 931 const val EXCEPTION_VULCAN_WEB_REQUEST = 932 const val EXCEPTION_PODLASIE_API_REQUEST = 940 +const val EXCEPTION_VULCAN_HEBE_REQUEST = 950 const val LOGIN_NO_ARGUMENTS = 1201 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt index 24444a58..269cf9ed 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt @@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogi import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginWeb import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain import pl.szczodrzynski.edziennik.data.api.models.LoginMethod @@ -98,11 +99,13 @@ val mobidziennikLoginMethods = listOf( const val LOGIN_TYPE_VULCAN = 4 const val LOGIN_MODE_VULCAN_API = 0 const val LOGIN_MODE_VULCAN_WEB = 1 +const val LOGIN_MODE_VULCAN_HEBE = 2 const val LOGIN_METHOD_VULCAN_WEB_MAIN = 100 const val LOGIN_METHOD_VULCAN_WEB_NEW = 200 const val LOGIN_METHOD_VULCAN_WEB_OLD = 300 const val LOGIN_METHOD_VULCAN_WEB_MESSAGES = 400 const val LOGIN_METHOD_VULCAN_API = 500 +const val LOGIN_METHOD_VULCAN_HEBE = 600 val vulcanLoginMethods = listOf( LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java) .withIsPossible { _, loginStore -> loginStore.hasLoginData("webHost") } @@ -117,9 +120,19 @@ val vulcanLoginMethods = listOf( .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN },*/ LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_API, VulcanLoginApi::class.java) - .withIsPossible { _, _ -> true } + .withIsPossible { _, loginStore -> + loginStore.mode == LOGIN_MODE_VULCAN_API + } .withRequiredLoginMethod { _, loginStore -> if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED + }, + + LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_HEBE, VulcanLoginHebe::class.java) + .withIsPossible { _, loginStore -> + loginStore.mode == LOGIN_MODE_VULCAN_HEBE + } + .withRequiredLoginMethod { _, loginStore -> + if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED } ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt index 882a2fa4..d494ec3d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt @@ -4,16 +4,15 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.currentTimeUnix +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Team -import pl.szczodrzynski.edziennik.isNotNullNorEmpty import pl.szczodrzynski.edziennik.utils.Utils -import pl.szczodrzynski.edziennik.values class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { @@ -26,12 +25,21 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app && apiFingerprint[symbol].isNotNullNorEmpty() && apiPrivateKey[symbol].isNotNullNorEmpty() && symbol.isNotNullNorEmpty() + fun isHebeLoginValid() = hebePublicKey.isNotNullNorEmpty() + && hebePrivateKey.isNotNullNorEmpty() + && symbol.isNotNullNorEmpty() override fun satisfyLoginMethods() { loginMethods.clear() + if (isWebMainLoginValid()) { + loginMethods += LOGIN_METHOD_VULCAN_WEB_MAIN + } if (isApiLoginValid()) { loginMethods += LOGIN_METHOD_VULCAN_API } + if (isHebeLoginValid()) { + loginMethods += LOGIN_METHOD_VULCAN_HEBE + } } init { @@ -55,6 +63,17 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app override fun generateUserCode() = "$schoolCode:$studentId" + fun buildDeviceId(): String { + val deviceId = app.deviceId.padStart(16, '0') + val loginStoreId = loginStore.id.toString(16).padStart(4, '0') + val symbol = symbol?.crc16()?.toString(16)?.take(2) ?: "00" + return deviceId.substring(0..7) + + "-" + deviceId.substring(8..11) + + "-" + deviceId.substring(12..15) + + "-" + loginStoreId + + "-" + symbol + "6f72616e7a" + } + /** * A UONET+ client symbol. * @@ -203,6 +222,27 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app get() { mApiPrivateKey = mApiPrivateKey ?: loginStore.getLoginData("apiPrivateKey", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiPrivateKey ?: mapOf() } set(value) { loginStore.putLoginData("apiPrivateKey", app.gson.toJson(value)); mApiPrivateKey = value } + /* _ _ _ _____ _____ + | | | | | | /\ | __ \_ _| + | |__| | ___| |__ ___ / \ | |__) || | + | __ |/ _ \ '_ \ / _ \ / /\ \ | ___/ | | + | | | | __/ |_) | __/ / ____ \| | _| |_ + |_| |_|\___|_.__/ \___| /_/ \_\_| |____*/ + private var mHebePublicKey: String? = null + var hebePublicKey: String? + get() { mHebePublicKey = mHebePublicKey ?: loginStore.getLoginData("hebePublicKey", null); return mHebePublicKey } + set(value) { loginStore.putLoginData("hebePublicKey", value); mHebePublicKey = value } + + private var mHebePrivateKey: String? = null + var hebePrivateKey: String? + get() { mHebePrivateKey = mHebePrivateKey ?: loginStore.getLoginData("hebePrivateKey", null); return mHebePrivateKey } + set(value) { loginStore.putLoginData("hebePrivateKey", value); mHebePrivateKey = value } + + private var mHebePublicHash: String? = null + var hebePublicHash: String? + get() { mHebePublicHash = mHebePublicHash ?: loginStore.getLoginData("hebePublicHash", null); return mHebePublicHash } + set(value) { loginStore.putLoginData("hebePublicHash", value); mHebePublicHash = value } + val apiUrl: String? get() { val url = when (apiToken[symbol]?.substring(0, 3)) { @@ -227,7 +267,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app return if (url != null) "$url/$symbol/" else loginStore.getLoginData("apiUrl", null) } - val fullApiUrl: String? + val fullApiUrl: String get() { return "$apiUrl$schoolSymbol/" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt new file mode 100644 index 00000000..cd8a9b77 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt @@ -0,0 +1,187 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data + +import android.os.Build +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.body.MediaTypeUtils +import im.wangchao.mhttp.callback.JsonCallbackHandler +import io.github.wulkanowy.signer.hebe.getSignatureHeaders +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection +import java.net.URLEncoder +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.* + +open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { + companion object { + const val TAG = "VulcanHebe" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + inline fun apiRequest( + tag: String, + endpoint: String, + method: Int = GET, + payload: JsonObject? = null, + baseUrl: Boolean = false, + crossinline onSuccess: (json: T, response: Response?) -> Unit + ) { + val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint" + + d(tag, "Request: Vulcan/Hebe - $url") + + val privateKey = data.hebePrivateKey + val publicHash = data.hebePublicHash + + if (privateKey == null || publicHash == null) { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + return + } + + val timestamp = ZonedDateTime.now(ZoneId.of("GMT")) + val timestampMillis = timestamp.toInstant().toEpochMilli() + val timestampIso = timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")) + + val finalPayload = if (payload != null) { + JsonObject( + "AppName" to VULCAN_HEBE_APP_NAME, + "AppVersion" to VULCAN_HEBE_APP_VERSION, + "CertificateId" to publicHash, + "Envelope" to payload, + "FirebaseToken" to data.app.config.sync.tokenVulcanHebe, + "API" to 1, + "RequestId" to UUID.randomUUID().toString(), + "Timestamp" to timestampMillis, + "TimestampFormatted" to timestampIso + ) + } else null + val jsonString = finalPayload?.toString() + + val headers = getSignatureHeaders( + publicHash, + privateKey, + jsonString, + endpoint, + timestamp + ) + + val callback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (json == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response) + ) + return + } + + val status = json.getJsonObject("Status") + if (status?.getInt("Code") != 0) { + data.error(ApiError(tag, ERROR_VULCAN_HEBE_OTHER) + .withResponse(response) + .withApiResponse(json.toString())) + } + + val envelope = when (T::class.java) { + JsonObject::class.java -> json.getJsonObject("Envelope") + JsonArray::class.java -> json.getJsonArray("Envelope") + else -> { + data.error(ApiError(tag, ERROR_RESPONSE_EMPTY) + .withResponse(response) + .withApiResponse(json) + ) + return + } + } + + try { + onSuccess(envelope as T, response) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_VULCAN_HEBE_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(json) + ) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable) + ) + } + } + + Request.builder() + .url(url) + .userAgent(VULCAN_HEBE_USER_AGENT) + .addHeader("vOS", "Android") + .addHeader("vDeviceModel", Build.MODEL) + .addHeader("vAPI", "1") + .apply { + headers.forEach { + addHeader(it.key, it.value) + } + when (method) { + GET -> get() + POST -> { + post() + setTextBody(jsonString, MediaTypeUtils.APPLICATION_JSON) + } + } + } + .allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST) + .allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN) + .allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE) + .callback(callback) + .build() + .enqueue() + } + + inline fun apiGet( + tag: String, + endpoint: String, + query: Map = mapOf(), + baseUrl: Boolean = false, + crossinline onSuccess: (json: T, response: Response?) -> Unit + ) { + val queryPath = query.map { it.key + "=" + URLEncoder.encode(it.value, "UTF-8") }.join("&") + apiRequest( + tag, + if (query.isNotEmpty()) "$endpoint?$queryPath" else endpoint, + baseUrl = baseUrl, + onSuccess = onSuccess + ) + } + + inline fun apiPost( + tag: String, + endpoint: String, + payload: JsonObject, + baseUrl: Boolean = false, + crossinline onSuccess: (json: T, response: Response?) -> Unit + ) { + apiRequest( + tag, + endpoint, + method = POST, + payload, + baseUrl = baseUrl, + onSuccess = onSuccess + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt index ed541c69..b670b12a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt @@ -4,14 +4,17 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin +import com.google.gson.JsonArray import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -25,6 +28,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { private val api = VulcanApi(data, null) private val web = VulcanWebMain(data, null) + private val hebe = VulcanHebe(data, null) private val profileList = mutableListOf() private val loginStoreId = data.loginStore.id private var firstProfileId = loginStoreId @@ -50,12 +54,18 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { checkSymbol(certificate) } } - else { + else if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) { registerDevice { EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) onSuccess() } } + else { + registerDeviceHebe { + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } } private fun checkSymbol(certificate: CufsCertificate) { @@ -103,7 +113,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { data.apiPin = data.apiPin.toMutableMap().also { it[symbol] = json.getString("PIN") } - registerDevice(onSuccess) + registerDeviceHebe(onSuccess) } } } @@ -197,4 +207,113 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { } } } + + private fun registerDeviceHebe(onSuccess: () -> Unit) { + VulcanLoginHebe(data) { + hebe.apiGet( + TAG, + VULCAN_HEBE_ENDPOINT_MAIN, + query = mapOf("lastSyncDate" to "null"), + baseUrl = true + ) { students: JsonArray, _ -> + if (students.isEmpty()) { + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore)) + onSuccess() + return@apiGet + } + + students.forEach { studentEl -> + val student = studentEl.asJsonObject + + val unit = student.getJsonObject("Unit") + //val constituentUnit = student.getJsonObject("ConstituentUnit") + val pupil = student.getJsonObject("Pupil") + val login = student.getJsonObject("Login") + val periods = student.getJsonArray("Periods")?.map { + it.asJsonObject + } ?: listOf() + + val period = periods.firstOrNull { + it.getBoolean("Current", false) + } ?: return@forEach + + val periodLevel = period.getInt("Level") ?: return@forEach + val semester1 = periods.firstOrNull { + it.getInt("Level") == periodLevel && it.getInt("Number") == 1 + } + val semester2 = periods.firstOrNull { + it.getInt("Level") == periodLevel && it.getInt("Number") == 2 + } + + val schoolSymbol = unit.getString("Symbol") ?: return@forEach + val schoolShort = unit.getString("Short") ?: return@forEach + val schoolCode = "${data.symbol}_$schoolSymbol" + val studentId = pupil.getInt("Id") ?: return@forEach + val studentLoginId = login.getInt("Id") ?: return@forEach + //val studentClassId = student.getInt("IdOddzial") ?: return@forEach + val studentClassName = student.getString("ClassDisplay") ?: return@forEach + val studentFirstName = pupil.getString("FirstName") ?: "" + val studentLastName = pupil.getString("Surname") ?: "" + val studentNameLong = "$studentFirstName $studentLastName".fixName() + val studentNameShort = "$studentFirstName ${studentLastName[0]}.".fixName() + val userLogin = login.getString("Value") ?: "" + + val studentSemesterId = period.getInt("Id") ?: return@forEach + val studentSemesterNumber = period.getInt("Number") ?: return@forEach + + val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true) + val accountName = if (isParent) + login.getString("DisplayName")?.fixName() + else null + + val dateSemester1Start = semester1 + ?.getJsonObject("Start") + ?.getString("Date") + ?.let { Date.fromY_m_d(it) } + val dateSemester2Start = semester2 + ?.getJsonObject("Start") + ?.getString("Date") + ?.let { Date.fromY_m_d(it) } + val dateYearEnd = semester2 + ?.getJsonObject("End") + ?.getString("Date") + ?.let { Date.fromY_m_d(it) } + + val profile = Profile( + firstProfileId++, + loginStoreId, + LOGIN_TYPE_VULCAN, + studentNameLong, + userLogin, + studentNameLong, + studentNameShort, + accountName + ).apply { + this.studentClassName = studentClassName + studentData["symbol"] = data.symbol + + studentData["studentId"] = studentId + studentData["studentLoginId"] = studentLoginId + studentData["studentSemesterId"] = studentSemesterId + studentData["studentSemesterNumber"] = studentSemesterNumber + studentData["semester1Id"] = semester1?.getInt("Id") ?: 0 + studentData["semester2Id"] = semester2?.getInt("Id") ?: 0 + studentData["schoolSymbol"] = schoolSymbol + studentData["schoolShort"] = schoolShort + studentData["schoolName"] = schoolCode + } + dateSemester1Start?.let { + profile.dateSemester1Start = it + profile.studentSchoolYearStart = it.year + } + dateSemester2Start?.let { profile.dateSemester2Start = it } + dateYearEnd?.let { profile.dateYearEnd = it } + + profileList.add(profile) + } + + onSuccess() + } + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt index 45c0153d..b92dea88 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.utils.Utils @@ -54,6 +55,10 @@ class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_login_vulcan_api) VulcanLoginApi(data) { onSuccess(loginMethodId) } } + LOGIN_METHOD_VULCAN_HEBE -> { + data.startProgress(R.string.edziennik_progress_login_vulcan_api) + VulcanLoginHebe(data) { onSuccess(loginMethodId) } + } } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt index ec84cd05..def1d7e3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt @@ -191,18 +191,6 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { } } - val deviceId = data.app.deviceId.padStart(16, '0') - val loginStoreId = data.loginStore.id.toString(16).padStart(4, '0') - val symbol = data.symbol?.crc16()?.toString(16)?.take(2) ?: "00" - val uuid = - deviceId.substring(0..7) + - "-" + deviceId.substring(8..11) + - "-" + deviceId.substring(12..15) + - "-" + loginStoreId + - "-" + symbol + "6f72616e7a" - - val deviceNameSuffix = " - nie usuwać" - val szkolnyApi = SzkolnyApi(data.app) val firebaseToken = szkolnyApi.runCatching({ getFirebaseToken("vulcan") @@ -216,8 +204,8 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { .addHeader("RequestMobileType", "RegisterDevice") .addParameter("PIN", data.apiPin[data.symbol]) .addParameter("TokenKey", data.apiToken[data.symbol]) - .addParameter("DeviceId", uuid) - .addParameter("DeviceName", VULCAN_API_DEVICE_NAME.take(50 - deviceNameSuffix.length) + deviceNameSuffix) + .addParameter("DeviceId", data.buildDeviceId()) + .addParameter("DeviceName", VULCAN_API_DEVICE_NAME) .addParameter("DeviceNameUser", "") .addParameter("DeviceDescription", "") .addParameter("DeviceSystemType", "Android") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt new file mode 100644 index 00000000..b2c27454 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt @@ -0,0 +1,105 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login + +import com.google.gson.JsonObject +import io.github.wulkanowy.signer.hebe.generateKeyPair +import pl.szczodrzynski.edziennik.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_DEVICE_NAME +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_REGISTER_NEW +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.isNotNullNorEmpty + +class VulcanLoginHebe(val data: DataVulcan, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "VulcanLoginHebe" + } + + init { run { + // i'm sure this does something useful + // not quite sure what, though + if (data.studentSemesterNumber == 1 && data.semester1Id == 0) + data.semester1Id = data.studentSemesterNumber + if (data.studentSemesterNumber == 2 && data.semester2Id == 0) + data.semester2Id = data.studentSemesterNumber + + copyFromLoginStore() + + if (data.profile != null && data.isApiLoginValid()) { + onSuccess() + } + else { + if (data.symbol.isNotNullNorEmpty() && data.apiToken[data.symbol].isNotNullNorEmpty() && data.apiPin[data.symbol].isNotNullNorEmpty()) { + loginWithToken() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + private fun copyFromLoginStore() { + data.loginStore.data.apply { + // map form inputs to the symbol + if (has("symbol")) { + data.symbol = getString("symbol") + remove("symbol") + } + if (has("deviceToken")) { + data.apiToken = data.apiToken.toMutableMap().also { + it[data.symbol] = getString("deviceToken") + } + remove("deviceToken") + } + if (has("devicePin")) { + data.apiPin = data.apiPin.toMutableMap().also { + it[data.symbol] = getString("devicePin") + } + remove("devicePin") + } + } + } + + private fun loginWithToken() { + val szkolnyApi = SzkolnyApi(data.app) + val hebe = VulcanHebe(data, null) + + if (data.hebePublicKey == null || data.hebePrivateKey == null || data.hebePublicHash == null) { + val (publicPem, privatePem, publicHash) = generateKeyPair() + data.hebePublicKey = publicPem + data.hebePrivateKey = privatePem + data.hebePublicHash = publicHash + } + + szkolnyApi.runCatching({ + data.app.config.sync.tokenVulcanHebe = getFirebaseToken("vulcan") + }, onError = { + // screw errors + }) + + hebe.apiPost( + TAG, + VULCAN_HEBE_ENDPOINT_REGISTER_NEW, + payload = JsonObject( + "OS" to "Android", + "PIN" to data.apiPin[data.symbol], + "Certificate" to data.hebePublicKey, + "CertificateType" to "RSA_PEM", + "DeviceModel" to VULCAN_API_DEVICE_NAME, + "SecurityToken" to data.apiToken[data.symbol], + "SelfIdentifier" to data.buildDeviceId(), + "CertificateThumbprint" to data.hebePublicHash + ), + baseUrl = true + ) { _: JsonObject, _ -> + data.apiToken = data.apiToken.toMutableMap().also { + it[data.symbol] = it[data.symbol]?.substring(0, 3) + } + data.loginStore.removeLoginData("apiPin") + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt index 2b3acd68..0ed90bdf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt @@ -182,6 +182,58 @@ object LoginInfo { ERROR_LOGIN_VULCAN_EXPIRED_TOKEN to R.string.login_error_expired_token ) ), + Mode( + loginMode = LOGIN_MODE_VULCAN_HEBE, + name = R.string.login_mode_vulcan_api, + icon = R.drawable.login_mode_vulcan_hebe, + hintText = R.string.login_mode_vulcan_api_hint, + guideText = R.string.login_mode_vulcan_api_guide, + isTesting = true, + credentials = listOf( + FormField( + keyName = "deviceToken", + name = R.string.login_hint_token, + icon = CommunityMaterial.Icon.cmd_code_braces, + emptyText = R.string.login_error_no_token, + invalidText = R.string.login_error_incorrect_token, + errorCodes = mapOf( + ERROR_LOGIN_VULCAN_INVALID_TOKEN to R.string.login_error_incorrect_token + ), + isRequired = true, + validationRegex = "[A-Z0-9]{5,12}", + caseMode = FormField.CaseMode.UPPER_CASE + ), + FormField( + keyName = "symbol", + name = R.string.login_hint_symbol, + icon = CommunityMaterial.Icon2.cmd_school, + emptyText = R.string.login_error_no_symbol, + invalidText = R.string.login_error_incorrect_symbol, + errorCodes = mapOf( + ERROR_LOGIN_VULCAN_INVALID_SYMBOL to R.string.login_error_incorrect_symbol + ), + isRequired = true, + validationRegex = "[a-z0-9_-]+", + caseMode = FormField.CaseMode.LOWER_CASE + ), + FormField( + keyName = "devicePin", + name = R.string.login_hint_pin, + icon = CommunityMaterial.Icon2.cmd_lock, + emptyText = R.string.login_error_no_pin, + invalidText = R.string.login_error_incorrect_pin, + errorCodes = mapOf( + ERROR_LOGIN_VULCAN_INVALID_PIN to R.string.login_error_incorrect_pin + ), + isRequired = true, + validationRegex = "[0-9]+", + caseMode = FormField.CaseMode.LOWER_CASE + ) + ), + errorCodes = mapOf( + ERROR_LOGIN_VULCAN_EXPIRED_TOKEN to R.string.login_error_expired_token + ) + ), Mode( loginMode = LOGIN_MODE_VULCAN_WEB, name = R.string.login_mode_vulcan_web, diff --git a/app/src/main/res/drawable/login_mode_vulcan_hebe.png b/app/src/main/res/drawable/login_mode_vulcan_hebe.png new file mode 100644 index 00000000..bea96dc7 Binary files /dev/null and b/app/src/main/res/drawable/login_mode_vulcan_hebe.png differ