diff --git a/.gitignore b/.gitignore index 25786692..1864dd01 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,5 @@ app/schemas/ signatures/ -app/.cxx \ No newline at end of file +app/.cxx +/i18n/ diff --git a/app/build.gradle b/app/build.gradle index ae884511..57b92b4a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -201,6 +201,11 @@ dependencies { implementation 'com.qifan.powerpermission:powerpermission:1.0.0' implementation 'com.qifan.powerpermission:powerpermission-coroutines:1.0.0' + + implementation 'com.github.kuba2k2.FSLogin:lib:master-SNAPSHOT' + implementation 'pl.droidsonroids:jspoon:1.3.2' + implementation "com.squareup.retrofit2:converter-scalars:2.8.1" + implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2" } repositories { mavenCentral() 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 81cd81cf..38f97834 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 @@ -113,5 +113,7 @@ const val VULCAN_API_ENDPOINT_MESSAGES_ADD = "mobile-api/Uczen.v3.Uczen/DodajWia const val VULCAN_API_ENDPOINT_PUSH = "mobile-api/Uczen.v3.Uczen/UstawPushToken" const val VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/WiadomosciZalacznik" 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 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 268da24e..1905c733 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 @@ -159,6 +159,15 @@ const val ERROR_VULCAN_API_MAINTENANCE = 340 const val ERROR_VULCAN_API_BAD_REQUEST = 341 const val ERROR_VULCAN_API_OTHER = 342 const val ERROR_VULCAN_ATTACHMENT_DOWNLOAD = 343 +const val ERROR_VULCAN_WEB_DATA_MISSING = 344 +const val ERROR_VULCAN_WEB_429 = 345 +const val ERROR_VULCAN_WEB_OTHER = 346 +const val ERROR_VULCAN_WEB_NO_CERTIFICATE = 347 +const val ERROR_VULCAN_WEB_NO_REGISTER = 348 +const val ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED = 349 +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_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401 const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402 @@ -209,5 +218,7 @@ const val EXCEPTION_IDZIENNIK_API_REQUEST = 914 const val EXCEPTION_EDUDZIENNIK_WEB_REQUEST = 920 const val EXCEPTION_EDUDZIENNIK_FILE_REQUEST = 921 const val ERROR_ONEDRIVE_DOWNLOAD = 930 +const val EXCEPTION_VULCAN_WEB_LOGIN = 931 +const val EXCEPTION_VULCAN_WEB_REQUEST = 932 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 66b764cd..527c86b1 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 @@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.Mobidzie 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.VulcanLoginWebMain import pl.szczodrzynski.edziennik.data.api.models.LoginMethod // librus @@ -103,11 +104,11 @@ const val LOGIN_METHOD_VULCAN_WEB_OLD = 300 const val LOGIN_METHOD_VULCAN_WEB_MESSAGES = 400 const val LOGIN_METHOD_VULCAN_API = 500 val vulcanLoginMethods = listOf( - /*LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java) - .withIsPossible { _, _ -> false } + LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java) + .withIsPossible { _, loginStore -> loginStore.hasLoginData("webHost") } .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, - LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_NEW, VulcanLoginWebNew::class.java) + /*LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_NEW, VulcanLoginWebNew::class.java) .withIsPossible { _, _ -> false } .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN }, @@ -118,7 +119,7 @@ val vulcanLoginMethods = listOf( LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_API, VulcanLoginApi::class.java) .withIsPossible { _, _ -> true } .withRequiredLoginMethod { _, loginStore -> - if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_NEW else LOGIN_METHOD_NOT_NEEDED + 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/Regexes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt index 57dceb0e..5da87c73 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt @@ -139,6 +139,9 @@ object Regexes { val VULCAN_SHIFT_ANNOTATION by lazy { """\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex() } + val VULCAN_WEB_PERMISSIONS by lazy { + """permissions: '([A-z0-9\/=+\-_]+?)'""".toRegex() + } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt index e47c7842..a93632b1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt @@ -59,7 +59,7 @@ class EdudziennikFirstLogin(val data: DataEdudziennik, val onSuccess: () -> Unit profileList.add(profile) } - EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) onSuccess() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/firstlogin/IdziennikFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/firstlogin/IdziennikFirstLogin.kt index 892c12c8..1a07c609 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/firstlogin/IdziennikFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/firstlogin/IdziennikFirstLogin.kt @@ -89,7 +89,7 @@ class IdziennikFirstLogin(val data: DataIdziennik, val onSuccess: () -> Unit) { profileList.add(profile) } - EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) onSuccess() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt index cc62a061..68f44269 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt @@ -33,7 +33,7 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) { val accounts = json.getJsonArray("accounts") if (accounts == null || accounts.size() < 1) { - EventBus.getDefault().post(FirstLoginFinishedEvent(listOf(), data.loginStore)) + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore)) onSuccess() return@portalGet } @@ -81,7 +81,7 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) { profileList.add(profile) } - EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) onSuccess() } } @@ -124,7 +124,7 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) { } profileList.add(profile) - EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) onSuccess() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt index 4857bd09..11b668f9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt @@ -85,7 +85,7 @@ class MobidziennikFirstLogin(val data: DataMobidziennik, val onSuccess: () -> Un profileList.add(profile) } - EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) onSuccess() } } 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 d326ebd4..fc11d73d 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 @@ -17,9 +17,13 @@ import pl.szczodrzynski.edziennik.values class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { + fun isWebMainLoginValid() = currentSemesterEndDate-30 > currentTimeUnix() + && apiFingerprint[symbol].isNotNullNorEmpty() + && apiPrivateKey[symbol].isNotNullNorEmpty() + && symbol.isNotNullNorEmpty() fun isApiLoginValid() = currentSemesterEndDate-30 > currentTimeUnix() - && apiCertificateKey.isNotNullNorEmpty() - && apiCertificatePrivate.isNotNullNorEmpty() + && apiFingerprint[symbol].isNotNullNorEmpty() + && apiPrivateKey[symbol].isNotNullNorEmpty() && symbol.isNotNullNorEmpty() override fun satisfyLoginMethods() { @@ -40,7 +44,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app id, name, Team.TYPE_CLASS, - "$schoolName:$name", + "$schoolCode:$name", -1 ) teamList.put(id, teamObject) @@ -48,7 +52,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app } } - override fun generateUserCode() = "$schoolName:$studentId" + override fun generateUserCode() = "$schoolCode:$studentId" /** * A UONET+ client symbol. @@ -59,8 +63,8 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app */ private var mSymbol: String? = null var symbol: String? - get() { mSymbol = mSymbol ?: loginStore.getLoginData("deviceSymbol", null); return mSymbol } - set(value) { loginStore.putLoginData("deviceSymbol", value); mSymbol = value } + get() { mSymbol = mSymbol ?: profile?.getStudentData("symbol", null); return mSymbol } + set(value) { profile?.putStudentData("symbol", value); mSymbol = value } /** * Group symbol/number of the student's school. @@ -75,16 +79,26 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app set(value) { profile?.putStudentData("schoolSymbol", value) ?: return; mSchoolSymbol = value } /** - * A school ID consisting of the [symbol] and [schoolSymbol]. + * Short name of the school, used in some places. + * + * ListaUczniow/JednostkaSprawozdawczaSkrot, e.g. "SP Wilkow" + */ + private var mSchoolShort: String? = null + var schoolShort: String? + get() { mSchoolShort = mSchoolShort ?: profile?.getStudentData("schoolShort", null); return mSchoolShort } + set(value) { profile?.putStudentData("schoolShort", value) ?: return; mSchoolShort = value } + + /** + * A school code consisting of the [symbol] and [schoolSymbol]. * * [symbol]_[schoolSymbol] * * e.g. "poznan_000088" */ - private var mSchoolName: String? = null - var schoolName: String? - get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } - set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } + private var mSchoolCode: String? = null + var schoolCode: String? + get() { mSchoolCode = mSchoolCode ?: profile?.getStudentData("schoolName", null); return mSchoolCode } + set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolCode = value } /** * ID of the student. @@ -154,45 +168,34 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app * After first login only 3 first characters are stored here. * This is later used to determine the API URL address. */ - private var mApiToken: String? = null - var apiToken: String? - get() { mApiToken = mApiToken ?: loginStore.getLoginData("deviceToken", null); return mApiToken } - set(value) { loginStore.putLoginData("deviceToken", value); mApiToken = value } + private var mApiToken: Map? = null + var apiToken: Map = mapOf() + get() { mApiToken = mApiToken ?: loginStore.getLoginData("apiToken", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiToken ?: mapOf() } + set(value) { loginStore.putLoginData("apiToken", app.gson.toJson(value)); mApiToken = value } /** * A mobile API registration PIN. * * After first login, this is removed and/or set to null. */ - private var mApiPin: String? = null - var apiPin: String? - get() { mApiPin = mApiPin ?: loginStore.getLoginData("devicePin", null); return mApiPin } - set(value) { loginStore.putLoginData("devicePin", value); mApiPin = value } + private var mApiPin: Map? = null + var apiPin: Map = mapOf() + get() { mApiPin = mApiPin ?: loginStore.getLoginData("apiPin", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiPin ?: mapOf() } + set(value) { loginStore.putLoginData("apiPin", app.gson.toJson(value)); mApiPin = value } - private var mApiCertificateKey: String? = null - var apiCertificateKey: String? - get() { mApiCertificateKey = mApiCertificateKey ?: loginStore.getLoginData("certificateKey", null); return mApiCertificateKey } - set(value) { loginStore.putLoginData("certificateKey", value); mApiCertificateKey = value } + private var mApiFingerprint: Map? = null + var apiFingerprint: Map = mapOf() + get() { mApiFingerprint = mApiFingerprint ?: loginStore.getLoginData("apiFingerprint", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiFingerprint ?: mapOf() } + set(value) { loginStore.putLoginData("apiFingerprint", app.gson.toJson(value)); mApiFingerprint = value } - /** - * This is not meant for normal usage. - * - * It provides a backward compatibility (<4.0) in order - * to migrate and use private keys instead of PFX. - */ - private var mApiCertificatePfx: String? = null - var apiCertificatePfx: String? - get() { mApiCertificatePfx = mApiCertificatePfx ?: loginStore.getLoginData("certificatePfx", null); return mApiCertificatePfx } - set(value) { loginStore.putLoginData("certificatePfx", value); mApiCertificatePfx = value } - - private var mApiCertificatePrivate: String? = null - var apiCertificatePrivate: String? - get() { mApiCertificatePrivate = mApiCertificatePrivate ?: loginStore.getLoginData("certificatePrivate", null); return mApiCertificatePrivate } - set(value) { loginStore.putLoginData("certificatePrivate", value); mApiCertificatePrivate = value } + private var mApiPrivateKey: Map? = null + var apiPrivateKey: Map = mapOf() + 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 } val apiUrl: String? get() { - val url = when (apiToken?.substring(0, 3)) { + val url = when (apiToken[symbol]?.substring(0, 3)) { "3S1" -> "https://lekcjaplus.vulcan.net.pl" "TA1" -> "https://uonetplus-komunikacja.umt.tarnow.pl" "OP1" -> "https://uonetplus-komunikacja.eszkola.opolskie.pl" @@ -217,4 +220,95 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app get() { return "$apiUrl$schoolSymbol/" } + + /* __ __ _ ______ _____ _ _ + \ \ / / | | | ____/ ____| | | (_) + \ \ /\ / /__| |__ | |__ | (___ | | ___ __ _ _ _ __ + \ \/ \/ / _ \ '_ \ | __| \___ \ | | / _ \ / _` | | '_ \ + \ /\ / __/ |_) | | | ____) | | |___| (_) | (_| | | | | | + \/ \/ \___|_.__/ |_| |_____/ |______\___/ \__, |_|_| |_| + __/ | + |__*/ + /** + * Federation Services login type. + * This might be one of: cufs, adfs, adfslight. + */ + var webType: String? + get() { mWebType = mWebType ?: loginStore.getLoginData("webType", null); return mWebType } + set(value) { loginStore.putLoginData("webType", value); mWebType = value } + private var mWebType: String? = null + + /** + * Web server providing the federation services login. + * If this is present, WEB_MAIN login is considered as available. + */ + var webHost: String? + get() { mWebHost = mWebHost ?: loginStore.getLoginData("webHost", null); return mWebHost } + set(value) { loginStore.putLoginData("webHost", value); mWebHost = value } + private var mWebHost: String? = null + + /** + * An ID used in ADFS & ADFSLight login types. + */ + var webAdfsId: String? + get() { mWebAdfsId = mWebAdfsId ?: loginStore.getLoginData("webAdfsId", null); return mWebAdfsId } + set(value) { loginStore.putLoginData("webAdfsId", value); mWebAdfsId = value } + private var mWebAdfsId: String? = null + + /** + * A domain override for ADFS Light. + */ + var webAdfsDomain: String? + get() { mWebAdfsDomain = mWebAdfsDomain ?: loginStore.getLoginData("webAdfsDomain", null); return mWebAdfsDomain } + set(value) { loginStore.putLoginData("webAdfsDomain", value); mWebAdfsDomain = value } + private var mWebAdfsDomain: String? = null + + var webIsHttpCufs: Boolean + get() { mWebIsHttpCufs = mWebIsHttpCufs ?: loginStore.getLoginData("webIsHttpCufs", false); return mWebIsHttpCufs ?: false } + set(value) { loginStore.putLoginData("webIsHttpCufs", value); mWebIsHttpCufs = value } + private var mWebIsHttpCufs: Boolean? = null + + var webIsScopedAdfs: Boolean + get() { mWebIsScopedAdfs = mWebIsScopedAdfs ?: loginStore.getLoginData("webIsScopedAdfs", false); return mWebIsScopedAdfs ?: false } + set(value) { loginStore.putLoginData("webIsScopedAdfs", value); mWebIsScopedAdfs = value } + private var mWebIsScopedAdfs: Boolean? = null + + var webEmail: String? + get() { mWebEmail = mWebEmail ?: loginStore.getLoginData("webEmail", null); return mWebEmail } + set(value) { loginStore.putLoginData("webEmail", value); mWebEmail = value } + private var mWebEmail: String? = null + var webUsername: String? + get() { mWebUsername = mWebUsername ?: loginStore.getLoginData("webUsername", null); return mWebUsername } + set(value) { loginStore.putLoginData("webUsername", value); mWebUsername = value } + private var mWebUsername: String? = null + var webPassword: String? + get() { mWebPassword = mWebPassword ?: loginStore.getLoginData("webPassword", null); return mWebPassword } + set(value) { loginStore.putLoginData("webPassword", value); mWebPassword = value } + private var mWebPassword: String? = null + + /** + * Expiry time of a certificate POSTed to a LoginEndpoint of the specific symbol. + * If the time passes, the certificate needs to be POSTed again (if valid) + * or re-generated. + */ + var webExpiryTime: Long + get() { mWebExpiryTime = mWebExpiryTime ?: profile?.getStudentData("webExpiryTime", 0L); return mWebExpiryTime ?: 0L } + set(value) { profile?.putStudentData("webExpiryTime", value); mWebExpiryTime = value } + private var mWebExpiryTime: Long? = null + + /** + * EfebSsoAuthCookie retrieved after posting a certificate + */ + var webAuthCookie: String? + get() { mWebAuthCookie = mWebAuthCookie ?: profile?.getStudentData("webAuthCookie", null); return mWebAuthCookie } + set(value) { profile?.putStudentData("webAuthCookie", value); mWebAuthCookie = value } + private var mWebAuthCookie: String? = null + + /** + * Permissions needed to get JSONs from home page + */ + var webPermissions: String? + get() { mWebPermissions = mWebPermissions ?: profile?.getStudentData("webPermissions", null); return mWebPermissions } + set(value) { profile?.putStudentData("webPermissions", value); mWebPermissions = value } + private var mWebPermissions: String? = null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanApi.kt index 172be3c0..c500e3e7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanApi.kt @@ -106,11 +106,11 @@ open class VulcanApi(open val data: DataVulcan, open val lastSync: Long?) { Request.builder() .url(url) .userAgent(VULCAN_API_USER_AGENT) - .addHeader("RequestCertificateKey", data.apiCertificateKey) + .addHeader("RequestCertificateKey", data.apiFingerprint[data.symbol]) .addHeader("RequestSignatureValue", try { signContent( - data.apiCertificatePrivate ?: "", + data.apiPrivateKey[data.symbol] ?: "", finalPayload.toString() ) } catch (e: Exception) {e.printStackTrace();""}) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanWebMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanWebMain.kt new file mode 100644 index 00000000..a2c0247a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanWebMain.kt @@ -0,0 +1,277 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-17. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.droidsonroids.jspoon.Jspoon +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.isNotNullNorBlank +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.models.Date +import java.io.File +import java.net.HttpURLConnection + +open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) { + companion object { + const val TAG = "VulcanWebMain" + const val WEB_MAIN = 0 + const val WEB_OLD = 1 + const val WEB_NEW = 2 + const val WEB_MESSAGES = 3 + const val STATE_SUCCESS = 0 + const val STATE_NO_REGISTER = 1 + const val STATE_LOGGED_OUT = 2 + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + private val certificateAdapter by lazy { + Jspoon.create().adapter(CufsCertificate::class.java) + } + + fun saveCertificate(certificate: String) { + val file = File(data.app.filesDir, "cert_"+(data.webUsername ?: data.webEmail)+".xml") + file.writeText(certificate) + } + + fun readCertificate(): String? { + val file = File(data.app.filesDir, "cert_"+(data.webUsername ?: data.webEmail)+".xml") + if (file.canRead()) + return file.readText() + return null + } + + fun parseCertificate(certificate: String): CufsCertificate { + val xml = certificate + .replace("<[a-z]+?:".toRegex(), "<") + .replace(" Unit): Boolean { + val cufsCertificate = parseCertificate(certificate) + + // check if the certificate is valid + if (Date.fromIso(cufsCertificate.expiryDate) < System.currentTimeMillis()) + return false + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (response?.headers()?.get("Location")?.contains("LoginEndpoint.aspx") == true + || response?.headers()?.get("Location")?.contains("?logout=true") == true) { + onResult(symbol, STATE_LOGGED_OUT) + return + } + if (text?.contains("LoginEndpoint.aspx?logout=true") == true) { + onResult(symbol, STATE_NO_REGISTER) + return + } + if (!validateCallback(text, response, jsonResponse = false)) { + return + } + onResult(symbol, STATE_SUCCESS) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("https://uonetplus.${data.webHost}/$symbol/LoginEndpoint.aspx") + .withClient(data.app.httpLazy) + .userAgent(SYSTEM_USER_AGENT) + .post() + .addParameter("wa", "wsignin1.0") + .addParameter("wctx", cufsCertificate.targetUrl) + .addParameter("wresult", certificate) + .allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST) + .allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN) + .allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE) + .allowErrorCode(429) + .callback(callback) + .build() + .enqueue() + + return true + } + + fun getStartPage(symbol: String = data.symbol ?: "default", postErrors: Boolean = true, onSuccess: (html: String, schoolSymbols: List) -> Unit) { + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (!validateCallback(text, response, jsonResponse = false) || text == null) { + return + } + + if (postErrors) { + when { + text.contains("status absolwenta") -> ERROR_VULCAN_WEB_GRADUATE_ACCOUNT + else -> null + }?.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withResponse(response) + .withApiResponse(text)) + return + } + } + + data.webPermissions = Regexes.VULCAN_WEB_PERMISSIONS.find(text)?.let { it[1] } + + val schoolSymbols = mutableListOf() + val clientUrl = "https://uonetplus-uczen.${data.webHost}/$symbol/" + var clientIndex = text.indexOf(clientUrl) + var count = 0 + while (clientIndex != -1 && count < 100) { + val startIndex = clientIndex + clientUrl.length + val endIndex = text.indexOf('/', startIndex = startIndex) + val schoolSymbol = text.substring(startIndex, endIndex) + schoolSymbols += schoolSymbol + clientIndex = text.indexOf(clientUrl, startIndex = endIndex) + count++ + } + + onSuccess(text, schoolSymbols) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("https://uonetplus.${data.webHost}/$symbol/Start.mvc/Index") + .userAgent(SYSTEM_USER_AGENT) + .get() + .allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST) + .allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN) + .allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE) + .allowErrorCode(429) + .callback(callback) + .build() + .enqueue() + } + + private fun validateCallback(text: String?, response: Response?, jsonResponse: Boolean = true): Boolean { + if (text == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return false + } + + if (response?.code() !in 200..302 || (jsonResponse && !text.startsWith("{"))) { + when { + text.contains("The custom error module") -> ERROR_VULCAN_WEB_429 + else -> ERROR_VULCAN_WEB_OTHER + }.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(text) + .withResponse(response)) + return false + } + } + + val cookies = data.app.cookieJar.getAll(data.webHost ?: "vulcan.net.pl") + val authCookie = cookies["EfebSsoAuthCookie"] + if ((authCookie == null || authCookie == "null") && data.webAuthCookie != null) { + data.app.cookieJar.set(data.webHost ?: "vulcan.net.pl", "EfebSsoAuthCookie", data.webAuthCookie) + } + else if (authCookie.isNotNullNorBlank() && authCookie != "null" && authCookie != data.webAuthCookie) { + data.webAuthCookie = authCookie + } + return true + } + + fun webGetJson( + tag: String, + webType: Int, + endpoint: String, + method: Int = POST, + parameters: Map = emptyMap(), + onSuccess: (json: JsonObject, response: Response?) -> Unit + ) { + val url = "https://" + when (webType) { + WEB_MAIN -> "uonetplus" + WEB_OLD -> "uonetplus-opiekun" + WEB_NEW -> "uonetplus-uczen" + WEB_MESSAGES -> "uonetplus-uzytkownik" + else -> "uonetplus" + } + ".${data.webHost}/${data.symbol}/$endpoint" + + Utils.d(tag, "Request: Vulcan/WebMain - $url") + + val payload = JsonObject() + parameters.map { (name, value) -> + when (value) { + is JsonObject -> payload.add(name, value) + is JsonArray -> payload.add(name, value) + is String -> payload.addProperty(name, value) + is Int -> payload.addProperty(name, value) + is Long -> payload.addProperty(name, value) + is Float -> payload.addProperty(name, value) + is Char -> payload.addProperty(name, value) + is Boolean -> payload.addProperty(name, value) + } + } + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (!validateCallback(text, response)) + return + + try { + val json = JsonParser().parse(text).asJsonObject + onSuccess(json, response) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_VULCAN_WEB_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url(url) + .userAgent(SYSTEM_USER_AGENT) + .apply { + when (method) { + GET -> get() + POST -> post() + } + } + .setJsonBody(payload) + .allowErrorCode(429) + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTimetable.kt index d8899dbf..70830865 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTimetable.kt @@ -80,7 +80,7 @@ class VulcanApiTimetable(override val data: DataVulcan, id, name, Team.TYPE_VIRTUAL, - "${data.schoolName}:$name", + "${data.schoolCode}:$name", teacherId ?: oldTeacherId ?: -1 ) data.teamList[id] = team 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 291cb37b..3d7e3aea 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 @@ -6,12 +6,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN -import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_STUDENT_LIST +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.VulcanWebMain import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi +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 import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.utils.models.Date @@ -21,19 +23,92 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { } private val api = VulcanApi(data, null) + private val web = VulcanWebMain(data, null) private val profileList = mutableListOf() + private val loginStoreId = data.loginStore.id + private var firstProfileId = loginStoreId + private val tryingSymbols = mutableListOf() init { - val loginStoreId = data.loginStore.id - val loginStoreType = LOGIN_TYPE_VULCAN - var firstProfileId = loginStoreId + if (data.loginStore.mode == LOGIN_MODE_VULCAN_WEB) { + VulcanLoginWebMain(data) { + val certificate = web.readCertificate() ?: run { + data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_CERTIFICATE)) + return@VulcanLoginWebMain + } + if (data.symbol != null && data.symbol != "default") { + tryingSymbols += data.symbol ?: "default" + } + else { + val cufsCertificate = web.parseCertificate(certificate) + tryingSymbols += cufsCertificate.userInstances + } + + checkSymbol(certificate) + } + } + else { + registerDevice { + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } + } + + private fun checkSymbol(certificate: String) { + if (tryingSymbols.isEmpty()) { + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + return + } + + val result = web.postCertificate(certificate, tryingSymbols.removeAt(0)) { symbol, state -> + when (state) { + VulcanWebMain.STATE_NO_REGISTER -> { + checkSymbol(certificate) + } + VulcanWebMain.STATE_LOGGED_OUT -> data.error(ApiError(TAG, ERROR_VULCAN_WEB_LOGGED_OUT)) + VulcanWebMain.STATE_SUCCESS -> { + webRegisterDevice(symbol) { + checkSymbol(certificate) + } + } + } + } + + // postCertificate returns false if the cert is not valid anymore + if (!result) { + data.error(ApiError(TAG, ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED) + .withApiResponse(certificate)) + } + } + + private fun webRegisterDevice(symbol: String, onSuccess: () -> Unit) { + web.getStartPage(symbol, postErrors = false) { _, schoolSymbols -> + data.symbol = symbol + val schoolSymbol = data.schoolSymbol ?: schoolSymbols.firstOrNull() + web.webGetJson(TAG, VulcanWebMain.WEB_NEW, "$schoolSymbol/$VULCAN_WEB_ENDPOINT_REGISTER_DEVICE") { result, _ -> + val json = result.getJsonObject("data") + data.symbol = symbol + data.apiToken = data.apiToken.toMutableMap().also { + it[symbol] = json.getString("TokenKey") + } + data.apiPin = data.apiPin.toMutableMap().also { + it[symbol] = json.getString("PIN") + } + registerDevice(onSuccess) + } + } + } + + private fun registerDevice(onSuccess: () -> Unit) { VulcanLoginApi(data) { - api.apiGet(TAG, VULCAN_API_ENDPOINT_STUDENT_LIST, baseUrl = true) { json, response -> + api.apiGet(TAG, VULCAN_API_ENDPOINT_STUDENT_LIST, baseUrl = true) { json, _ -> val students = json.getJsonArray("Data") if (students == null || students.isEmpty()) { - EventBus.getDefault().post(FirstLoginFinishedEvent(listOf(), data.loginStore)) + EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore)) onSuccess() return@apiGet } @@ -42,7 +117,8 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { val student = studentEl.asJsonObject val schoolSymbol = student.getString("JednostkaSprawozdawczaSymbol") ?: return@forEach - val schoolName = "${data.symbol}_$schoolSymbol" + val schoolShort = student.getString("JednostkaSprawozdawczaSkrot") ?: return@forEach + val schoolCode = "${data.symbol}_$schoolSymbol" val studentId = student.getInt("Id") ?: return@forEach val studentLoginId = student.getInt("UzytkownikLoginId") ?: return@forEach val studentClassId = student.getInt("IdOddzial") ?: return@forEach @@ -80,7 +156,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { val profile = Profile( firstProfileId++, loginStoreId, - loginStoreType, + LOGIN_TYPE_VULCAN, studentNameLong, userLogin, studentNameLong, @@ -88,13 +164,16 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { accountName ).apply { this.studentClassName = studentClassName + studentData["symbol"] = data.symbol + studentData["studentId"] = studentId studentData["studentLoginId"] = studentLoginId studentData["studentClassId"] = studentClassId studentData["studentSemesterId"] = studentSemesterId studentData["studentSemesterNumber"] = studentSemesterNumber studentData["schoolSymbol"] = schoolSymbol - studentData["schoolName"] = schoolName + studentData["schoolShort"] = schoolShort + studentData["schoolName"] = schoolCode studentData["currentSemesterEndDate"] = currentSemesterEndDate } dateSemester1Start?.let { @@ -107,7 +186,6 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { profileList.add(profile) } - EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) onSuccess() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/CufsCertificate.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/CufsCertificate.kt new file mode 100644 index 00000000..5e2e299f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/CufsCertificate.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-17. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login + +import pl.droidsonroids.jspoon.annotation.Selector + +class CufsCertificate { + @Selector(value = "EndpointReference Address") + var targetUrl: String = "" + + @Selector(value = "Lifetime Created") + var createdDate: String = "" + + @Selector(value = "Lifetime Expires") + var expiryDate: String = "" + + @Selector(value = "Attribute[AttributeName=UserInstance] AttributeValue") + var userInstances: List = listOf() +} 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 4c11caf3..45c0153d 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_WEB_MAIN import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.utils.Utils @@ -45,6 +46,10 @@ class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) { } Utils.d(TAG, "Using login method $loginMethodId") when (loginMethodId) { + LOGIN_METHOD_VULCAN_WEB_MAIN -> { + data.startProgress(R.string.edziennik_progress_login_vulcan_web_main) + VulcanLoginWebMain(data) { onSuccess(loginMethodId) } + } LOGIN_METHOD_VULCAN_API -> { data.startProgress(R.string.edziennik_progress_login_vulcan_api) VulcanLoginApi(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 4385b18a..cdf93c5d 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 @@ -10,14 +10,11 @@ import im.wangchao.mhttp.Request import im.wangchao.mhttp.Response import im.wangchao.mhttp.callback.JsonCallbackHandler import io.github.wulkanowy.signer.android.getPrivateKeyFromCert -import pl.szczodrzynski.edziennik.currentTimeUnix +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.api.VulcanApiUpdateSemester import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getJsonObject -import pl.szczodrzynski.edziennik.getString -import pl.szczodrzynski.edziennik.isNotNullNorEmpty import pl.szczodrzynski.edziennik.utils.Utils.d import java.net.HttpURLConnection.HTTP_BAD_REQUEST import java.util.* @@ -29,28 +26,14 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { } init { run { + copyFromLoginStore() + if (data.profile != null && data.isApiLoginValid()) { onSuccess() } else { - // < v4.0 - PFX to Private Key migration - if (data.apiCertificatePfx.isNotNullNorEmpty()) { - try { - data.apiCertificatePrivate = getPrivateKeyFromCert( - if (data.apiToken?.get(0) == 'F') VULCAN_API_PASSWORD_FAKELOG else VULCAN_API_PASSWORD, - data.apiCertificatePfx ?: "" - ) - data.loginStore.removeLoginData("certificatePfx") - } catch (e: Throwable) { - e.printStackTrace() - } finally { - onSuccess() - return@run - } - } - - if (data.apiCertificateKey.isNotNullNorEmpty() - && data.apiCertificatePrivate.isNotNullNorEmpty() + if (data.apiFingerprint[data.symbol].isNotNullNorEmpty() + && data.apiPrivateKey[data.symbol].isNotNullNorEmpty() && data.symbol.isNotNullNorEmpty()) { // (see data.isApiLoginValid()) // the semester end date is over @@ -58,7 +41,7 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { return@run } - if (data.symbol.isNotNullNorEmpty() && data.apiToken.isNotNullNorEmpty() && data.apiPin.isNotNullNorEmpty()) { + if (data.symbol.isNotNullNorEmpty() && data.apiToken[data.symbol].isNotNullNorEmpty() && data.apiPin[data.symbol].isNotNullNorEmpty()) { loginWithToken() } else { @@ -67,6 +50,64 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { } }} + private fun copyFromLoginStore() { + data.loginStore.data.apply { + // < v4.0 - PFX to Private Key migration + if (has("certificatePfx")) { + try { + val privateKey = getPrivateKeyFromCert( + if (data.apiToken[data.symbol]?.get(0) == 'F') VULCAN_API_PASSWORD_FAKELOG else VULCAN_API_PASSWORD, + getString("certificatePfx") ?: "" + ) + data.apiPrivateKey = mapOf( + data.symbol to privateKey + ) + remove("certificatePfx") + } catch (e: Throwable) { + e.printStackTrace() + } + } + + // 4.0 - new login form - copy user input to profile + if (has("symbol")) { + data.symbol = getString("symbol") + remove("symbol") + } + + // 4.0 - before Vulcan Web impl - migrate from strings to Map of Symbol to String + if (has("deviceSymbol")) { + data.symbol = getString("deviceSymbol") + remove("deviceSymbol") + } + if (has("certificateKey")) { + data.apiFingerprint = data.apiFingerprint.toMutableMap().also { + it[data.symbol] = getString("certificateKey") + } + remove("certificateKey") + } + if (has("certificatePrivate")) { + data.apiPrivateKey = data.apiPrivateKey.toMutableMap().also { + it[data.symbol] = getString("certificatePrivate") + } + remove("certificatePrivate") + } + + // map form inputs to the 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() { d(TAG, "Request: Vulcan/Login/Api - ${data.apiUrl}/$VULCAN_API_ENDPOINT_CERTIFICATE") @@ -118,14 +159,22 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { return } - data.apiCertificateKey = cert.getString("CertyfikatKlucz") - data.apiToken = data.apiToken?.substring(0, 3) - data.apiCertificatePrivate = getPrivateKeyFromCert( - if (data.apiToken?.get(0) == 'F') VULCAN_API_PASSWORD_FAKELOG else VULCAN_API_PASSWORD, + val privateKey = getPrivateKeyFromCert( + if (data.apiToken[data.symbol]?.get(0) == 'F') VULCAN_API_PASSWORD_FAKELOG else VULCAN_API_PASSWORD, cert.getString("CertyfikatPfx") ?: "" ) + + data.apiFingerprint = data.apiFingerprint.toMutableMap().also { + it[data.symbol] = cert.getString("CertyfikatKlucz") + } + data.apiToken = data.apiToken.toMutableMap().also { + it[data.symbol] = it[data.symbol]?.substring(0, 3) + } + data.apiPrivateKey = data.apiPrivateKey.toMutableMap().also { + it[data.symbol] = privateKey + } data.loginStore.removeLoginData("certificatePfx") - data.loginStore.removeLoginData("devicePin") + data.loginStore.removeLoginData("apiPin") onSuccess() } @@ -136,14 +185,26 @@ 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ć" + Request.builder() .url("${data.apiUrl}$VULCAN_API_ENDPOINT_CERTIFICATE") .userAgent(VULCAN_API_USER_AGENT) .addHeader("RequestMobileType", "RegisterDevice") - .addParameter("PIN", data.apiPin) - .addParameter("TokenKey", data.apiToken) - .addParameter("DeviceId", UUID.randomUUID().toString()) - .addParameter("DeviceName", VULCAN_API_DEVICE_NAME) + .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("DeviceNameUser", "") .addParameter("DeviceDescription", "") .addParameter("DeviceSystemType", "Android") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginWebMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginWebMain.kt new file mode 100644 index 00000000..04a6f17e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginWebMain.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-16. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login + +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.fslogin.FSLogin +import pl.szczodrzynski.fslogin.realm.CufsRealm + +class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "VulcanLoginWebMain" + } + + private val web by lazy { VulcanWebMain(data, null) } + + init { run { + copyFromLoginStore() + + if (data.profile != null && data.isWebMainLoginValid()) { + onSuccess() + } + else { + if (data.symbol.isNotNullNorEmpty() + && data.webType.isNotNullNorEmpty() + && data.webHost.isNotNullNorEmpty() + && (data.webEmail.isNotNullNorEmpty() || data.webUsername.isNotNullNorEmpty()) + && data.webPassword.isNotNullNorEmpty()) { + try { + val success = loginWithCredentials() + if (!success) + data.error(ApiError(TAG, ERROR_VULCAN_WEB_DATA_MISSING)) + } catch (e: Exception) { + data.error(ApiError(TAG, EXCEPTION_VULCAN_WEB_LOGIN) + .withThrowable(e)) + } + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + private fun copyFromLoginStore() { + data.loginStore.data.apply { + // 4.0 - new login form - copy user input to profile + if (has("symbol")) { + data.symbol = getString("symbol") + remove("symbol") + } + } + } + + private fun loginWithCredentials(): Boolean { + val realm = when (data.webType) { + "cufs" -> CufsRealm( + host = data.webHost ?: return false, + symbol = data.symbol ?: "default", + httpCufs = data.webIsHttpCufs + ) + "adfs" -> CufsRealm( + host = data.webHost ?: return false, + symbol = data.symbol ?: "default", + httpCufs = data.webIsHttpCufs + ).toAdfsRealm(id = data.webAdfsId ?: return false) + "adfslight" -> CufsRealm( + host = data.webHost ?: return false, + symbol = data.symbol ?: "default", + httpCufs = data.webIsHttpCufs + ).toAdfsLightRealm( + id = data.webAdfsId ?: return false, + domain = data.webAdfsDomain ?: "adfslight", + isScoped = data.webIsScopedAdfs + ) + else -> return false + } + + val fsLogin = FSLogin(data.app.http, debug = App.debugMode) + fsLogin.performLogin( + realm = realm, + username = data.webUsername ?: data.webEmail ?: return false, + password = data.webPassword ?: return false, + onSuccess = { certificate -> + web.saveCertificate(certificate.wresult) + + // auto-post certificate when not first login + if (data.profile != null && data.symbol != null && data.symbol != "default") { + val result = web.postCertificate(certificate.wresult, data.symbol ?: "default") { _, state -> + when (state) { + VulcanWebMain.STATE_SUCCESS -> { + web.getStartPage { _, _ -> onSuccess() } + } + VulcanWebMain.STATE_NO_REGISTER -> data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_REGISTER)) + VulcanWebMain.STATE_LOGGED_OUT -> data.error(ApiError(TAG, ERROR_VULCAN_WEB_LOGGED_OUT)) + } + } + // postCertificate returns false if the cert is not valid anymore + if (!result) { + data.error(ApiError(TAG, ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED) + .withApiResponse(certificate.wresult)) + } + } + else { + // first login - succeed immediately + onSuccess() + } + }, + onFailure = { errorText -> + // TODO + data.error(ApiError(TAG, 0).withThrowable(RuntimeException(errorText))) + } + ) + + return true + } +} 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 29624d40..04e9bf16 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 @@ -152,7 +152,7 @@ object LoginInfo { caseMode = Credential.CaseMode.UPPER_CASE ), Credential( - keyName = "deviceSymbol", + keyName = "symbol", name = R.string.login_hint_symbol, icon = CommunityMaterial.Icon2.cmd_school, emptyText = R.string.login_error_no_symbol, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt index ab76327e..930ee367 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt @@ -91,7 +91,7 @@ class LoginProgressFragment : Fragment(), CoroutineScope { } } - @Subscribe(threadMode = ThreadMode.MAIN) + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onFirstLoginFinishedEvent(event: FirstLoginFinishedEvent) { if (event.profileList.isEmpty()) { MaterialAlertDialogBuilder(activity) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt index c0aab663..04029d36 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt @@ -85,7 +85,7 @@ class LoginSyncFragment : Fragment(), CoroutineScope { ).concat(" ") } - @Subscribe(threadMode = ThreadMode.MAIN) + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onSyncFinishedEvent(event: ApiTaskAllFinishedEvent) { nav.navigate(R.id.loginFinishFragment, finishArguments, activity.navOptions) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java index c1d32538..88323bc1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import java.text.DateFormat; import java.util.Calendar; import java.util.Locale; +import java.util.TimeZone; import pl.szczodrzynski.edziennik.ExtensionsKt; import pl.szczodrzynski.edziennik.R; @@ -108,7 +109,11 @@ public class Date implements Comparable { public static long fromIso(String dateTime) { try { - return Date.fromY_m_d(dateTime).combineWith(new Time(Integer.parseInt(dateTime.substring(11, 13)), Integer.parseInt(dateTime.substring(14, 16)), Integer.parseInt(dateTime.substring(17, 19)))); + Calendar c = Calendar.getInstance(); + c.set(Integer.parseInt(dateTime.substring(0, 4)), Integer.parseInt(dateTime.substring(5, 7)) - 1, Integer.parseInt(dateTime.substring(8, 10)), Integer.parseInt(dateTime.substring(11, 13)), Integer.parseInt(dateTime.substring(14, 16)), Integer.parseInt(dateTime.substring(17, 19))); + c.set(Calendar.MILLISECOND, 0); + c.setTimeZone(TimeZone.getTimeZone("UTC")); + return c.getTimeInMillis(); } catch (Exception e) { return System.currentTimeMillis(); diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 63a4607b..e6d90882 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1230,4 +1230,7 @@ yesterday You\'re offline. Try enabling Wi-Fi or mobile data. Internet connection + In order to download the file, you have to grant file storage permission for the application.\n\nClick OK to grant the permission. + You denied the required permissions for the application.\n\nIn order to grant the permission, open the Permissions screen for Szkolny.eu in phone settings.\n\nClick OK to open app settings now. + Required permissions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5eb7fd82..8812f92c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1288,19 +1288,19 @@ Połączenie sieciowe Jaki masz e-dziennik w szkole? Wybierz z jakiego e-dziennika korzysta Twoja szkoła. Jeśli masz kilka kont w różnych dziennikach, będziesz mógł je dodać później. - Librus/Synergia + Librus/Synergia Zaloguj używając e-maila Musisz posiadać konto Librus Rodzina Zaloguj używając loginu i hasła Użyj loginu w postaci \"9874123u\" Logowanie przez platformę VULCAN Oświata w Radomiu oraz Innowacyjny Tarnobrzeg - Vulcan UONET+ + Vulcan UONET+ Użyj tokenu, symbolu i kodu PIN Zarejestruj urządzenie na stronie dziennika Vulcan Użyj e-maila/nazwy użytkownika i hasła Zaloguj danymi, które podajesz na stronie e-dziennika VULCAN - MobiDziennik + MobiDziennik Zaloguj nazwą serwera, loginem i hasłem Podaj dane, których używasz na stronie e-dziennika W jaki sposób się logujesz do dziennika? @@ -1316,4 +1316,5 @@ Podaj dane, których używasz do logowania na stronie MobiDziennika. Jako adres serwera możesz wpisać adres strony internetowej, na której masz MobiDziennik. Dodaj nowego ucznia Zaloguj konto ucznia/rodzica w aplikacji + Logowanie do dziennika Vulcan...