[Vulcan/Hebe] Add hebe API login implementation.

This commit is contained in:
Kuba Szczodrzyński 2021-02-19 13:08:44 +01:00
parent 73ff09052c
commit 2fcff33bd6
14 changed files with 578 additions and 23 deletions

View File

@ -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}"

View File

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

View File

@ -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<Int>? = null
var tokenMobidziennikList: List<Int>
@ -112,6 +116,10 @@ class ConfigSync(private val config: Config) {
var tokenVulcanList: List<Int>
get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() }
set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value }
private var mTokenVulcanHebeList: List<Int>? = null
var tokenVulcanHebeList: List<Int>
get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() }
set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value }
private var mRegisterAvailability: Map<String, RegisterAvailabilityStatus>? = null
var registerAvailability: Map<String, RegisterAvailabilityStatus>

View File

@ -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}"

View File

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

View File

@ -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,7 +120,17 @@ 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
}

View File

@ -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/"
}

View File

@ -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 <reified T> 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 <reified T> apiGet(
tag: String,
endpoint: String,
query: Map<String, String> = 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 <reified T> 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
)
}
}

View File

@ -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<Profile>()
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()
}
}
}
}

View File

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

View File

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

View File

@ -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()
}
}
}

View File

@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB