[LibSocial] Authorization support

Koitharu 11 months ago
parent 5856681753
commit 7b1a0b8d0d
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -35,8 +35,17 @@ public abstract class MangaLoaderContext {
* @param script JavaScript source code * @param script JavaScript source code
* @return execution result as string, may be null * @return execution result as string, may be null
*/ */
@Deprecated("Provide a base url")
public abstract suspend fun evaluateJs(script: String): String? public abstract suspend fun evaluateJs(script: String): String?
/**
* Execute JavaScript code and return result
* @param script JavaScript source code
* @param baseUrl url of page script will be executed in context of
* @return execution result as string, may be null
*/
public abstract suspend fun evaluateJs(baseUrl: String, script: String): String?
/** /**
* Open [url] in browser for some external action (e.g. captcha solving or non cookie-based authorization) * Open [url] in browser for some external action (e.g. captcha solving or non cookie-based authorization)
*/ */

@ -18,7 +18,7 @@ public interface MangaParserAuthProvider {
* Quick check if user is logged in. * Quick check if user is logged in.
* In most case you should check for cookies in [MangaLoaderContext.cookieJar]. * In most case you should check for cookies in [MangaLoaderContext.cookieJar].
*/ */
public val isAuthorized: Boolean public suspend fun isAuthorized(): Boolean
/** /**
* Fetch and return current user`s name or login. * Fetch and return current user`s name or login.

@ -35,8 +35,7 @@ internal class BatoToParser(context: MangaLoaderContext) : LegacyPagedMangaParse
override val authUrl: String override val authUrl: String
get() = "https://${domain}/signin" get() = "https://${domain}/signin"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean {
get() {
return context.cookieJar.getCookies(domain).any { return context.cookieJar.getCookies(domain).any {
it.name.contains("skey") it.name.contains("skey")
} }

@ -37,10 +37,13 @@ internal class ExHentaiParser(
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST) override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val configKeyDomain: ConfigKey.Domain override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain( get() {
val isAuthorized = checkAuth()
return ConfigKey.Domain(
if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED, if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED,
if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED, if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED,
) )
}
override val authUrl: String override val authUrl: String
get() = "https://${domain}/bounce_login.php" get() = "https://${domain}/bounce_login.php"
@ -59,22 +62,7 @@ internal class ExHentaiParser(
isAuthorSearchSupported = true, isAuthorSearchSupported = true,
) )
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean = checkAuth()
get() {
val authorized = isAuthorized(DOMAIN_UNAUTHORIZED)
if (authorized) {
if (!isAuthorized(DOMAIN_AUTHORIZED)) {
context.cookieJar.copyCookies(
DOMAIN_UNAUTHORIZED,
DOMAIN_AUTHORIZED,
authCookies,
)
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder")
}
return true
}
return false
}
init { init {
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2") context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2")
@ -484,4 +472,20 @@ internal class ExHentaiParser(
} }
acc or cat acc or cat
} }
private fun checkAuth(): Boolean {
val authorized = isAuthorized(DOMAIN_UNAUTHORIZED)
if (authorized) {
if (!isAuthorized(DOMAIN_AUTHORIZED)) {
context.cookieJar.copyCookies(
DOMAIN_UNAUTHORIZED,
DOMAIN_AUTHORIZED,
authCookies,
)
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder")
}
return true
}
return false
}
} }

@ -49,8 +49,7 @@ internal abstract class MangaFireParser(
override val authUrl: String override val authUrl: String
get() = "https://${domain}" get() = "https://${domain}"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean {
get() {
return context.cookieJar.getCookies(domain).any { return context.cookieJar.getCookies(domain).any {
it.value.contains("user") it.value.contains("user")
} }

@ -36,8 +36,7 @@ internal class MangaReaderToParser(context: MangaLoaderContext) :
override val authUrl: String override val authUrl: String
get() = "https://${domain}/home" get() = "https://${domain}/home"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean {
get() {
return context.cookieJar.getCookies(domain).any { return context.cookieJar.getCookies(domain).any {
it.name.contains("connect.sid") it.name.contains("connect.sid")
} }

@ -30,8 +30,8 @@ internal class WeebCentral(context: MangaLoaderContext) : LegacyMangaParser(cont
override val authUrl: String override val authUrl: String
get() = "https://$domain" get() = "https://$domain"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean =
get() = context.cookieJar.getCookies(domain).any { it.name == "access_token" } context.cookieJar.getCookies(domain).any { it.name == "access_token" }
override suspend fun getUsername(): String { override suspend fun getUsername(): String {
return webClient.httpGet("https://$domain/users/me/profiles") return webClient.httpGet("https://$domain/users/me/profiles")
@ -180,7 +180,9 @@ internal class WeebCentral(context: MangaLoaderContext) : LegacyMangaParser(cont
.toHttpUrl() .toHttpUrl()
.pathSegments[1] .pathSegments[1]
val author = document.select("div:contains(author) a").eachText().joinToString().nullIfEmpty() val author = document.select("div:contains(author) a").eachText().joinToString().nullIfEmpty()
val title = element.selectFirst("div.text-ellipsis.truncate.text-white.text-center.text-lg.z-20.w-\\[90\\%\\]")?.text() val title =
element.selectFirst("div.text-ellipsis.truncate.text-white.text-center.text-lg.z-20.w-\\[90\\%\\]")
?.text()
?: "No name" ?: "No name"
Manga( Manga(
id = generateUid(mangaId), id = generateUid(mangaId),

@ -45,8 +45,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) :
override val authUrl: String override val authUrl: String
get() = "https://${getDomain("account")}/login?site=seiga" get() = "https://${getDomain("account")}/login?site=seiga"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(getDomain("seiga")).any {
get() = context.cookieJar.getCookies(getDomain("seiga")).any {
it.name == "user_session" it.name == "user_session"
} }

@ -82,8 +82,7 @@ internal abstract class MadaraParser(
override val authUrl: String override val authUrl: String
get() = "https://${domain}" get() = "https://${domain}"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean {
get() {
return context.cookieJar.getCookies(domain).any { return context.cookieJar.getCookies(domain).any {
it.name.contains("wordpress_logged_in") it.name.contains("wordpress_logged_in")
} }

@ -25,8 +25,7 @@ internal class NudeMoonParser(
override val authUrl: String override val authUrl: String
get() = "https://${domain}/index.php" get() = "https://${domain}/index.php"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean {
get() {
return context.cookieJar.getCookies(domain).any { return context.cookieJar.getCookies(domain).any {
it.name == "fusion_user" it.name == "fusion_user"
} }
@ -61,7 +60,7 @@ internal class NudeMoonParser(
val url = when { val url = when {
!filter.query.isNullOrEmpty() -> { !filter.query.isNullOrEmpty() -> {
if (!isAuthorized) { if (!isAuthorized()) {
throw AuthRequiredException(source) throw AuthRequiredException(source)
} }
"https://$domain/search?stext=${filter.query.urlEncoded()}&rowstart=$offset" "https://$domain/search?stext=${filter.query.urlEncoded()}&rowstart=$offset"

@ -50,8 +50,7 @@ internal class RemangaParser(
SortOrder.NEWEST, SortOrder.NEWEST,
) )
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean {
get() {
return context.cookieJar.getCookies(domain).any { return context.cookieJar.getCookies(domain).any {
it.name == "user" it.name == "user"
} }

@ -21,8 +21,8 @@ internal class AllHentaiParser(
"2023.allhen.online", "2023.allhen.online",
) )
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean =
get() = super.isAuthorized || context.cookieJar.getCookies(domain).any { it.name == "remember_me" } super.isAuthorized() || context.cookieJar.getCookies(domain).any { it.name == "remember_me" }
override val authUrl: String override val authUrl: String
get() { get() {

@ -74,8 +74,7 @@ internal abstract class GroupleParser(
return "https://grouple.co/internal/auth/sso?siteId=$siteId&=targetUri=$targetUri" return "https://grouple.co/internal/auth/sso?siteId=$siteId&=targetUri=$targetUri"
} }
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean = hasAuthCookie()
get() = context.cookieJar.getCookies(domain).any { it.name == "gwt" }
override val filterCapabilities: MangaListFilterCapabilities override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities( get() = MangaListFilterCapabilities(
@ -556,7 +555,7 @@ internal abstract class GroupleParser(
throw AuthRequiredException(source) throw AuthRequiredException(source)
} }
if (code == HttpURLConnection.HTTP_NOT_FOUND) { if (code == HttpURLConnection.HTTP_NOT_FOUND) {
if (!isAuthorized) { if (!hasAuthCookie()) {
closeQuietly() closeQuietly()
throw AuthRequiredException(source) throw AuthRequiredException(source)
} else { } else {
@ -565,4 +564,6 @@ internal abstract class GroupleParser(
} }
return this return this
} }
private fun hasAuthCookie() = context.cookieJar.getCookies(domain).any { it.name == "gwt" }
} }

@ -26,8 +26,7 @@ internal abstract class ChanParser(
override val authUrl: String override val authUrl: String
get() = "https://${domain}" get() = "https://${domain}"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(domain).any { it.name == "dle_user_id" }
get() = context.cookieJar.getCookies(domain).any { it.name == "dle_user_id" }
override val filterCapabilities: MangaListFilterCapabilities override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities( get() = MangaListFilterCapabilities(

@ -5,6 +5,8 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
@ -31,9 +33,9 @@ internal abstract class LibSocialParser(
override val authUrl: String override val authUrl: String
get() = "https://$domain/ru/front/auth" get() = "https://$domain/ru/front/auth"
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean {
get() = runBlocking { val token = getAuthData()?.optJSONObject("token")?.getStringOrNull("access_token")
runCatchingCancellable { getAuthData() }.getOrNull() != null return !token.isNullOrEmpty()
} }
override suspend fun getUsername(): String = getAuthData() override suspend fun getUsername(): String = getAuthData()
@ -65,6 +67,18 @@ internal abstract class LibSocialParser(
availableStates = EnumSet.allOf(MangaState::class.java), availableStates = EnumSet.allOf(MangaState::class.java),
) )
override fun intercept(chain: Interceptor.Chain): Response {
val token = runBlocking { getAuthData() }?.optJSONObject("token")?.getStringOrNull("access_token")
return if (!token.isNullOrEmpty()) {
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.build()
chain.proceed(request)
} else {
super.intercept(chain)
}
}
private val statesMap = intObjectMapOf( private val statesMap = intObjectMapOf(
1, MangaState.ONGOING, 1, MangaState.ONGOING,
2, MangaState.FINISHED, 2, MangaState.FINISHED,
@ -374,7 +388,8 @@ internal abstract class LibSocialParser(
} }
private suspend fun getAuthData(): JSONObject? { private suspend fun getAuthData(): JSONObject? {
return JSONObject(WebViewHelper(context, domain).getLocalStorageValue("auth") ?: return null) val raw = WebViewHelper(context).getLocalStorageValue(domain, "auth") ?: return null
return JSONObject(raw.unescapeJson().removeSurrounding('"'))
} }
protected companion object { protected companion object {

@ -54,8 +54,8 @@ internal class CMangaParser(context: MangaLoaderContext) :
override val authUrl: String override val authUrl: String
get() = domain get() = domain
override val isAuthorized: Boolean override suspend fun isAuthorized(): Boolean =
get() = context.cookieJar.getCookies(domain).any { it.name == "login_password" } context.cookieJar.getCookies(domain).any { it.name == "login_password" }
override suspend fun getUsername(): String { override suspend fun getUsername(): String {
val userId = webClient.httpGet("https://$domain").parseRaw() val userId = webClient.httpGet("https://$domain").parseRaw()

@ -4,10 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
public class WebViewHelper( public class WebViewHelper(
private val context: MangaLoaderContext, private val context: MangaLoaderContext,
private val domain: String,
) { ) {
public suspend fun getLocalStorageValue(key: String): String? { public suspend fun getLocalStorageValue(domain: String, key: String): String? {
return context.evaluateJs("window.localStorage.getItem(\"$key\")") return context.evaluateJs("$SCHEME_HTTPS://$domain/", "window.localStorage.getItem(\"$key\")")
} }
} }

@ -40,7 +40,10 @@ internal object MangaLoaderContextMock : MangaLoaderContext() {
loadTestCookies() loadTestCookies()
} }
override suspend fun evaluateJs(script: String): String? { @Deprecated("Provide a base url")
override suspend fun evaluateJs(script: String): String? = evaluateJs("", script)
override suspend fun evaluateJs(baseUrl: String, script: String): String? {
return QuackContext.create().use { return QuackContext.create().use {
it.evaluate(script)?.toString() it.evaluate(script)?.toString()
} }

Loading…
Cancel
Save