Add workarounds to detect not mapped urls as NotLoggedIn errors in multi threaded envs

This commit is contained in:
Mikołaj Pich 2024-05-12 17:32:29 +02:00
parent ce84113020
commit 6fff07e52a
No known key found for this signature in database
7 changed files with 75 additions and 22 deletions

View file

@ -439,6 +439,11 @@ public final class io/github/wulkanowy/sdk/scrapper/exception/VulcanClientError
public class io/github/wulkanowy/sdk/scrapper/exception/VulcanException : io/github/wulkanowy/sdk/scrapper/exception/ScrapperException {
}
public final class io/github/wulkanowy/sdk/scrapper/exception/VulcanServerError : io/github/wulkanowy/sdk/scrapper/exception/VulcanException {
public final fun getDoc ()Lorg/jsoup/nodes/Document;
public final fun getHttpCode ()I
}
public final class io/github/wulkanowy/sdk/scrapper/grades/Grade {
public static final field Companion Lio/github/wulkanowy/sdk/scrapper/grades/Grade$Companion;
public field date Ljava/time/LocalDate;

View file

@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.scrapper.messages.Mailbox
import io.github.wulkanowy.sdk.scrapper.messages.Recipient
import io.github.wulkanowy.sdk.scrapper.messages.RecipientType
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.Jsoup
import org.slf4j.LoggerFactory
@ -201,7 +202,7 @@ internal fun String.md5(): String {
return digest.toHexString()
}
internal fun HttpUrl.mapModuleUrls(moduleHost: String, appVersion: String?): HttpUrl {
internal fun HttpUrl.mapModuleUrl(moduleHost: String, appVersion: String?): HttpUrl {
val pathSegmentIndex = getPathIndexByModuleHost(moduleHost)
val pathKey = pathSegments.getOrNull(pathSegmentIndex)
val mappedPath = Scrapper.endpointsMap[appVersion]
@ -221,6 +222,20 @@ internal fun HttpUrl.mapModuleUrls(moduleHost: String, appVersion: String?): Htt
}
}
internal fun isAnyMappingAvailable(url: String): Boolean {
val host = url.toHttpUrl().host
val module = when {
MessagesModuleHost in host -> MessagesModuleHost
StudentPlusModuleHost in host -> StudentPlusModuleHost
StudentModuleHost in host -> StudentModuleHost
else -> null
} ?: return false
return Scrapper.endpointsMap.keys.any { appVersion ->
url.toHttpUrl().mapModuleUrl(module, appVersion).toString() !== url
}
}
internal fun getPathIndexByModuleHost(moduleHost: String): Int = when (moduleHost) {
StudentPlusModuleHost -> 3
StudentModuleHost, MessagesModuleHost -> 2

View file

@ -0,0 +1,9 @@
package io.github.wulkanowy.sdk.scrapper.exception
import org.jsoup.nodes.Document
class VulcanServerError internal constructor(
message: String,
val doc: Document,
val httpCode: Int,
) : VulcanException(message)

View file

@ -11,12 +11,14 @@ import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.ADFSLightScoped
import io.github.wulkanowy.sdk.scrapper.Scrapper.LoginType.STANDARD
import io.github.wulkanowy.sdk.scrapper.attachVToken
import io.github.wulkanowy.sdk.scrapper.exception.VulcanClientError
import io.github.wulkanowy.sdk.scrapper.exception.VulcanServerError
import io.github.wulkanowy.sdk.scrapper.getModuleHeadersFromDocument
import io.github.wulkanowy.sdk.scrapper.isAnyMappingAvailable
import io.github.wulkanowy.sdk.scrapper.login.LoginResult
import io.github.wulkanowy.sdk.scrapper.login.ModuleHeaders
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException
import io.github.wulkanowy.sdk.scrapper.login.UrlGenerator
import io.github.wulkanowy.sdk.scrapper.mapModuleUrls
import io.github.wulkanowy.sdk.scrapper.mapModuleUrl
import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.SELECTOR_ADFS
import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.SELECTOR_ADFS_CARDS
import io.github.wulkanowy.sdk.scrapper.repository.AccountRepository.Companion.SELECTOR_ADFS_LIGHT
@ -71,8 +73,9 @@ internal class AutoLoginInterceptor(
val response = try {
chain.proceed(request.attachModuleHeaders())
} catch (e: Throwable) {
if (e is VulcanClientError) {
checkHttpErrorResponse(e, url)
when (e) {
is VulcanClientError -> checkHttpErrorResponse(e, url)
is VulcanServerError -> checkServerError(e, url)
}
throw e
}
@ -163,7 +166,7 @@ internal class AutoLoginInterceptor(
}
val headers = headersByHost[moduleHost]
val mappedUrl = url.mapModuleUrls(moduleHost, headers?.appVersion)
val mappedUrl = url.mapModuleUrl(moduleHost, headers?.appVersion)
logger.info("X-V-AppVersion: ${headers?.appVersion}")
@ -214,18 +217,27 @@ internal class AutoLoginInterceptor(
// new error style
val isCodeMatch = response.code == HttpURLConnection.HTTP_OK
val isJsonContent = bodyContent.startsWith("{")
val isSubdomainMatch = "uonetplus-uczen" in url
if (isCodeMatch && isJsonContent && isSubdomainMatch) {
runCatching { json.decodeFromString<ApiResponse<Unit?>>(bodyContent) }
.onFailure { logger.error("AutoLoginInterceptor: Can't deserialize new style error content body", it) }
.onSuccess {
it.feedback?.message?.let { errorMessage ->
if ("Brak uprawnień" in errorMessage) {
throw NotLoggedInException(errorMessage)
}
val isStudentModuleSubdomain = StudentModuleHost in url
if (isCodeMatch && isJsonContent && isStudentModuleSubdomain) {
checkResponseStudentModule(bodyContent)
}
}
private fun checkResponseStudentModule(bodyContent: String) {
runCatching { json.decodeFromString<ApiResponse<Unit?>>(bodyContent) }
.onFailure { logger.error("AutoLoginInterceptor: Can't deserialize new style error content body", it) }
.onSuccess {
it.feedback?.message?.let { errorMessage ->
if ("Brak uprawnień" in errorMessage) {
throw NotLoggedInException(errorMessage)
}
// workaround - access resource before request mapping
if ("was not found on controller" in errorMessage && headersByHost[StudentModuleHost] == null) {
throw NotLoggedInException(errorMessage)
}
}
}
}
}
private fun checkHttpErrorResponse(error: VulcanClientError, url: String) {
@ -236,6 +248,17 @@ internal class AutoLoginInterceptor(
}
}
private fun checkServerError(error: VulcanServerError, url: String) {
val isCodeMatch = error.httpCode == HttpURLConnection.HTTP_OK
val isSubdomainMatch = MessagesModuleHost in url || StudentModuleHost in url
val isMappable = isAnyMappingAvailable(url)
// workaround - access resource before request mapping
if (isCodeMatch && isSubdomainMatch && isMappable) {
throw NotLoggedInException(error.message.orEmpty())
}
}
/**
* @see [https://github.com/square/retrofit/issues/3110#issuecomment-536248102]
*/

View file

@ -7,6 +7,7 @@ import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException
import io.github.wulkanowy.sdk.scrapper.exception.TemporarilyDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.VulcanException
import io.github.wulkanowy.sdk.scrapper.exception.VulcanServerError
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -91,7 +92,7 @@ internal class ErrorInterceptor(
}
when (doc.title()) {
"Błąd" -> throw VulcanException(doc.body().text(), httpCode)
"Błąd" -> throw VulcanServerError(doc.body().text(), doc, httpCode)
"Błąd strony" -> throw VulcanException(doc.select(".errorMessage").text(), httpCode)
"Logowanie" -> throw AccountPermissionException(
buildString {

View file

@ -16,7 +16,7 @@ import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.sdk.scrapper.login.LoginHelper
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException
import io.github.wulkanowy.sdk.scrapper.login.UrlGenerator
import io.github.wulkanowy.sdk.scrapper.mapModuleUrls
import io.github.wulkanowy.sdk.scrapper.mapModuleUrl
import io.github.wulkanowy.sdk.scrapper.register.Diary
import io.github.wulkanowy.sdk.scrapper.register.HomePageResponse
import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent
@ -251,7 +251,7 @@ internal class RegisterRepository(
}
val diaryUrl = (url.generate(UrlGenerator.Site.STUDENT) + "UczenDziennik.mvc/Get")
.toHttpUrl()
.mapModuleUrls(StudentModuleHost, appVersion)
.mapModuleUrl(StudentModuleHost, appVersion)
return student
.getSchoolInfo(url = diaryUrl.toString())
@ -330,7 +330,7 @@ internal class RegisterRepository(
}
val cacheUrl = (url.generate(UrlGenerator.Site.STUDENT) + "UczenCache.mvc/Get")
.toHttpUrl()
.mapModuleUrls(StudentModuleHost, appVersion)
.mapModuleUrl(StudentModuleHost, appVersion)
val userCache = student.getUserCache(
url = cacheUrl.toString(),
@ -347,11 +347,11 @@ internal class RegisterRepository(
val contextUrl = (baseStudentPlus + "api/Context").toHttpUrl()
val contextVToken = contextUrl.getMatchedVToken(StudentPlusModuleHost, moduleHeaders)
val mappedContextUrl = contextUrl.mapModuleUrls(StudentPlusModuleHost, moduleHeaders.appVersion)
val mappedContextUrl = contextUrl.mapModuleUrl(StudentPlusModuleHost, moduleHeaders.appVersion)
val semestersUrl = (baseStudentPlus + "api/OkresyKlasyfikacyjne").toHttpUrl()
val semestersVToken = semestersUrl.getMatchedVToken(StudentPlusModuleHost, moduleHeaders)
val mappedSemestersUrl = semestersUrl.mapModuleUrls(StudentPlusModuleHost, moduleHeaders.appVersion)
val mappedSemestersUrl = semestersUrl.mapModuleUrl(StudentPlusModuleHost, moduleHeaders.appVersion)
return studentPlus
.getContextByUrl(vToken = contextVToken, url = mappedContextUrl.toString()).students

View file

@ -102,7 +102,6 @@ internal class ServiceManager(
private val interceptors: MutableList<Pair<Interceptor, Boolean>> = mutableListOf(
HttpLoggingInterceptor().setLevel(logLevel) to true,
ErrorInterceptor(cookieJarCabinet) to false,
AutoLoginInterceptor(
loginType = loginType,
cookieJarCabinet = cookieJarCabinet,
@ -113,6 +112,7 @@ internal class ServiceManager(
headersByHost = headersByHost,
loginLock = loginLock,
) to false,
ErrorInterceptor(cookieJarCabinet) to false,
UserAgentInterceptor(androidVersion, buildTag, userAgentTemplate) to false,
HttpErrorInterceptor() to false,
)