forked from github/szkolny
[API/Librus] Fix login. (#176)
This commit is contained in:
parent
8177d4aa2d
commit
07ab1b984f
@ -26,9 +26,10 @@ val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp"
|
|||||||
const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"
|
const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"
|
||||||
const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP"
|
const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP"
|
||||||
const val LIBRUS_REDIRECT_URL = "app://librus"
|
const val LIBRUS_REDIRECT_URL = "app://librus"
|
||||||
const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code"
|
const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/konto-librus/redirect/dru"
|
||||||
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action"
|
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/konto-librus/login/action"
|
||||||
const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"
|
const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"
|
||||||
|
const val LIBRUS_HEADER = "pl.librus.synergiaDru2"
|
||||||
|
|
||||||
const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login
|
const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login
|
||||||
const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts"
|
const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts"
|
||||||
@ -59,9 +60,6 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="
|
|||||||
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
|
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
|
||||||
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
|
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
|
||||||
|
|
||||||
const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR"
|
|
||||||
const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login"
|
|
||||||
|
|
||||||
|
|
||||||
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
|
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
|
||||||
|
|
||||||
|
@ -24,6 +24,25 @@ object Regexes {
|
|||||||
"""^\[META:([A-z0-9-&=]+)]""".toRegex()
|
"""^\[META:([A-z0-9-&=]+)]""".toRegex()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val HTML_INPUT_HIDDEN by lazy {
|
||||||
|
"""<input .*?type="hidden".+?>""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_INPUT_NAME by lazy {
|
||||||
|
"""name="(.+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_INPUT_VALUE by lazy {
|
||||||
|
"""value="(.+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_CSRF_TOKEN by lazy {
|
||||||
|
"""name="csrf-token" content="([A-z0-9=+/\-_]+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_FORM_ACTION by lazy {
|
||||||
|
"""<form .*?action="(.+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_RECAPTCHA_KEY by lazy {
|
||||||
|
"""data-sitekey="(.+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {
|
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {
|
||||||
|
@ -24,6 +24,9 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
private const val TAG = "LoginLibrusPortal"
|
private const val TAG = "LoginLibrusPortal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loop failsafe
|
||||||
|
private var loginPerformed = false
|
||||||
|
|
||||||
init { run {
|
init { run {
|
||||||
if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) {
|
if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) {
|
||||||
data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE))
|
data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE))
|
||||||
@ -33,6 +36,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
|
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
|
||||||
return@run
|
return@run
|
||||||
}
|
}
|
||||||
|
loginPerformed = false
|
||||||
|
|
||||||
// succeed having a non-expired access token and a refresh token
|
// succeed having a non-expired access token and a refresh token
|
||||||
if (data.isPortalLoginValid()) {
|
if (data.isPortalLoginValid()) {
|
||||||
@ -58,18 +62,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
private fun authorize(url: String?) {
|
private fun authorize(url: String, referer: String? = null) {
|
||||||
d(TAG, "Request: Librus/Login/Portal - $url")
|
d(TAG, "Request: Librus/Login/Portal - $url")
|
||||||
|
|
||||||
Request.builder()
|
Request.builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.userAgent(LIBRUS_USER_AGENT)
|
.userAgent(LIBRUS_USER_AGENT)
|
||||||
|
.also {
|
||||||
|
if (referer != null)
|
||||||
|
it.addHeader("Referer", referer)
|
||||||
|
}
|
||||||
|
.addHeader("X-Requested-With", LIBRUS_HEADER)
|
||||||
.withClient(data.app.httpLazy)
|
.withClient(data.app.httpLazy)
|
||||||
.callback(object : TextCallbackHandler() {
|
.callback(object : TextCallbackHandler() {
|
||||||
override fun onSuccess(text: String, response: Response) {
|
override fun onSuccess(text: String, response: Response) {
|
||||||
val location = response.headers().get("Location")
|
val location = response.headers().get("Location")
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
|
val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([^&?]+)", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
|
||||||
when {
|
when {
|
||||||
authMatcher.find() -> {
|
authMatcher.find() -> {
|
||||||
accessToken(authMatcher.group(1), null)
|
accessToken(authMatcher.group(1), null)
|
||||||
@ -83,16 +92,31 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
authorize(location)
|
authorize(location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return
|
||||||
val csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(text)
|
}
|
||||||
if (csrfMatcher.find()) {
|
|
||||||
login(csrfMatcher.group(1) ?: "")
|
if (checkError(text, response))
|
||||||
} else {
|
return
|
||||||
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING)
|
|
||||||
.withResponse(response)
|
var loginUrl = if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL
|
||||||
.withApiResponse(text))
|
val csrfToken = Regexes.HTML_CSRF_TOKEN.find(text)?.get(1) ?: ""
|
||||||
|
|
||||||
|
for (match in Regexes.HTML_FORM_ACTION.findAll(text)) {
|
||||||
|
val form = match.value.lowercase()
|
||||||
|
if ("login" in form && "post" in form) {
|
||||||
|
loginUrl = match[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val params = mutableMapOf<String, String>()
|
||||||
|
for (match in Regexes.HTML_INPUT_HIDDEN.findAll(text)) {
|
||||||
|
val input = match.value
|
||||||
|
val name = Regexes.HTML_INPUT_NAME.find(input)?.get(1) ?: continue
|
||||||
|
val value = Regexes.HTML_INPUT_VALUE.find(input)?.get(1) ?: continue
|
||||||
|
params[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
login(url = loginUrl, referer = url, csrfToken, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(response: Response, throwable: Throwable) {
|
override fun onFailure(response: Response, throwable: Throwable) {
|
||||||
@ -105,8 +129,54 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
.enqueue()
|
.enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun login(csrfToken: String) {
|
private fun checkError(text: String, response: Response): Boolean {
|
||||||
d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}")
|
when {
|
||||||
|
text.contains("librus_account_settings_main") -> return false
|
||||||
|
text.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
|
||||||
|
text.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
||||||
|
text.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
||||||
|
else -> null // no error for now
|
||||||
|
}?.let { errorCode ->
|
||||||
|
data.error(ApiError(TAG, errorCode)
|
||||||
|
.withApiResponse(text)
|
||||||
|
.withResponse(response))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("robotem" in text || "g-recaptcha" in text || "captchaValidate" in text) {
|
||||||
|
val siteKey = Regexes.HTML_RECAPTCHA_KEY.find(text)?.get(1)
|
||||||
|
if (siteKey == null) {
|
||||||
|
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR)
|
||||||
|
.withApiResponse(text)
|
||||||
|
.withResponse(response))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
data.requireUserAction(
|
||||||
|
type = UserActionRequiredEvent.Type.RECAPTCHA,
|
||||||
|
params = Bundle(
|
||||||
|
"siteKey" to siteKey,
|
||||||
|
"referer" to response.request().url().toString(),
|
||||||
|
"userAgent" to LIBRUS_USER_AGENT,
|
||||||
|
),
|
||||||
|
errorText = R.string.notification_user_action_required_captcha_librus,
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login(
|
||||||
|
url: String,
|
||||||
|
referer: String,
|
||||||
|
csrfToken: String?,
|
||||||
|
params: Map<String, String>,
|
||||||
|
) {
|
||||||
|
if (loginPerformed) {
|
||||||
|
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d(TAG, "Request: Librus/Login/Portal - $url")
|
||||||
|
|
||||||
val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null)
|
val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null)
|
||||||
val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L)
|
val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L)
|
||||||
@ -116,67 +186,46 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
Request.builder()
|
Request.builder()
|
||||||
.url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL)
|
.url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL)
|
||||||
.userAgent(LIBRUS_USER_AGENT)
|
.userAgent(LIBRUS_USER_AGENT)
|
||||||
|
.addHeader("X-Requested-With", LIBRUS_HEADER)
|
||||||
|
.addHeader("Referer", referer)
|
||||||
|
.withClient(data.app.httpLazy)
|
||||||
.addParameter("email", data.portalEmail)
|
.addParameter("email", data.portalEmail)
|
||||||
.addParameter("password", data.portalPassword)
|
.addParameter("password", data.portalPassword)
|
||||||
.also {
|
.also {
|
||||||
if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */)
|
if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */)
|
||||||
it.addParameter("g-recaptcha-response", recaptchaCode)
|
it.addParameter("g-recaptcha-response", recaptchaCode)
|
||||||
|
if (csrfToken != null)
|
||||||
|
it.addHeader("X-CSRF-TOKEN", csrfToken)
|
||||||
|
for ((key, value) in params) {
|
||||||
|
it.addParameter(key, value)
|
||||||
}
|
}
|
||||||
.addHeader("X-CSRF-TOKEN", csrfToken)
|
}
|
||||||
.allowErrorCode(HTTP_BAD_REQUEST)
|
.contentType(MediaTypeUtils.APPLICATION_FORM)
|
||||||
.allowErrorCode(HTTP_FORBIDDEN)
|
|
||||||
.contentType(MediaTypeUtils.APPLICATION_JSON)
|
|
||||||
.post()
|
.post()
|
||||||
.callback(object : JsonCallbackHandler() {
|
.callback(object : TextCallbackHandler() {
|
||||||
override fun onSuccess(json: JsonObject?, response: Response) {
|
override fun onSuccess(text: String?, response: Response) {
|
||||||
|
loginPerformed = true
|
||||||
val location = response.headers()?.get("Location")
|
val location = response.headers()?.get("Location")
|
||||||
if (location == "$LIBRUS_REDIRECT_URL?command=close") {
|
if (location == "$LIBRUS_REDIRECT_URL?command=close") {
|
||||||
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
|
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
|
||||||
.withApiResponse(json)
|
.withApiResponse(text)
|
||||||
.withResponse(response))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json == null) {
|
|
||||||
if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
|
|
||||||
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED)
|
|
||||||
.withResponse(response))
|
.withResponse(response))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (text == null) {
|
||||||
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
|
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
|
||||||
.withResponse(response))
|
.withResponse(response))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val error = if (response.code() == 200) null else
|
|
||||||
json.getJsonArray("errors")?.getString(0)
|
|
||||||
?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString
|
|
||||||
|
|
||||||
if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) {
|
authorize(
|
||||||
data.requireUserAction(
|
url = location
|
||||||
type = UserActionRequiredEvent.Type.RECAPTCHA,
|
?: if (data.fakeLogin)
|
||||||
params = Bundle(
|
FAKE_LIBRUS_AUTHORIZE
|
||||||
"siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY,
|
else
|
||||||
"referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER,
|
LIBRUS_AUTHORIZE_URL,
|
||||||
),
|
referer = referer,
|
||||||
errorText = R.string.notification_user_action_required_captcha_librus,
|
|
||||||
)
|
)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
error?.let { code ->
|
|
||||||
when {
|
|
||||||
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
|
|
||||||
code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
|
||||||
code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
|
||||||
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
|
|
||||||
}.let { errorCode ->
|
|
||||||
data.error(ApiError(TAG, errorCode)
|
|
||||||
.withApiResponse(json)
|
|
||||||
.withResponse(response))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(response: Response, throwable: Throwable) {
|
override fun onFailure(response: Response, throwable: Throwable) {
|
||||||
|
Loading…
Reference in New Issue
Block a user