[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
* @return execution result as string, may be null
*/
@Deprecated("Provide a base url")
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)
*/

@ -18,7 +18,7 @@ public interface MangaParserAuthProvider {
* Quick check if user is logged in.
* 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.

@ -35,12 +35,11 @@ internal class BatoToParser(context: MangaLoaderContext) : LegacyPagedMangaParse
override val authUrl: String
get() = "https://${domain}/signin"
override val isAuthorized: Boolean
get() {
return context.cookieJar.getCookies(domain).any {
it.name.contains("skey")
}
override suspend fun isAuthorized(): Boolean {
return context.cookieJar.getCookies(domain).any {
it.name.contains("skey")
}
}
override suspend fun getUsername(): String {
val body = webClient.httpGet("https://${domain}/account/profiles").parseHtml().body()

@ -37,10 +37,13 @@ internal class ExHentaiParser(
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain(
if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED,
if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED,
)
get() {
val isAuthorized = checkAuth()
return ConfigKey.Domain(
if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED,
if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED,
)
}
override val authUrl: String
get() = "https://${domain}/bounce_login.php"
@ -59,22 +62,7 @@ internal class ExHentaiParser(
isAuthorSearchSupported = true,
)
override val isAuthorized: Boolean
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
}
override suspend fun isAuthorized(): Boolean = checkAuth()
init {
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2")
@ -484,4 +472,20 @@ internal class ExHentaiParser(
}
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,12 +49,11 @@ internal abstract class MangaFireParser(
override val authUrl: String
get() = "https://${domain}"
override val isAuthorized: Boolean
get() {
return context.cookieJar.getCookies(domain).any {
it.value.contains("user")
}
override suspend fun isAuthorized(): Boolean {
return context.cookieJar.getCookies(domain).any {
it.value.contains("user")
}
}
override suspend fun getUsername(): String {
val body = webClient.httpGet("https://${domain}/user/profile").parseHtml().body()

@ -36,12 +36,11 @@ internal class MangaReaderToParser(context: MangaLoaderContext) :
override val authUrl: String
get() = "https://${domain}/home"
override val isAuthorized: Boolean
get() {
return context.cookieJar.getCookies(domain).any {
it.name.contains("connect.sid")
}
override suspend fun isAuthorized(): Boolean {
return context.cookieJar.getCookies(domain).any {
it.name.contains("connect.sid")
}
}
// It will be easier to connect to a manga page, as the source redirects to a lot of advertising.
override suspend fun getUsername(): String {

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

@ -45,10 +45,9 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) :
override val authUrl: String
get() = "https://${getDomain("account")}/login?site=seiga"
override val isAuthorized: Boolean
get() = context.cookieJar.getCookies(getDomain("seiga")).any {
it.name == "user_session"
}
override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(getDomain("seiga")).any {
it.name == "user_session"
}
override suspend fun getUsername(): String {
val body = webClient.httpGet("https://${getDomain("app")}/my/apps").parseHtml().body()

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

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

@ -50,12 +50,11 @@ internal class RemangaParser(
SortOrder.NEWEST,
)
override val isAuthorized: Boolean
get() {
return context.cookieJar.getCookies(domain).any {
it.name == "user"
}
override suspend fun isAuthorized(): Boolean {
return context.cookieJar.getCookies(domain).any {
it.name == "user"
}
}
private val regexLastUrlPath = Regex("/[^/]+/?$")

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

@ -74,8 +74,7 @@ internal abstract class GroupleParser(
return "https://grouple.co/internal/auth/sso?siteId=$siteId&=targetUri=$targetUri"
}
override val isAuthorized: Boolean
get() = context.cookieJar.getCookies(domain).any { it.name == "gwt" }
override suspend fun isAuthorized(): Boolean = hasAuthCookie()
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
@ -511,7 +510,7 @@ internal abstract class GroupleParser(
id = generateUid(url),
url = if (fullUrl.contains("one-way.work")) {
// domain that does not need a token
fullUrl.substringBefore("?")
fullUrl.substringBefore("?")
} else {
fullUrl
},
@ -556,7 +555,7 @@ internal abstract class GroupleParser(
throw AuthRequiredException(source)
}
if (code == HttpURLConnection.HTTP_NOT_FOUND) {
if (!isAuthorized) {
if (!hasAuthCookie()) {
closeQuietly()
throw AuthRequiredException(source)
} else {
@ -565,4 +564,6 @@ internal abstract class GroupleParser(
}
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
get() = "https://${domain}"
override val isAuthorized: Boolean
get() = context.cookieJar.getCookies(domain).any { it.name == "dle_user_id" }
override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(domain).any { it.name == "dle_user_id" }
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(

@ -5,6 +5,8 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
@ -31,10 +33,10 @@ internal abstract class LibSocialParser(
override val authUrl: String
get() = "https://$domain/ru/front/auth"
override val isAuthorized: Boolean
get() = runBlocking {
runCatchingCancellable { getAuthData() }.getOrNull() != null
}
override suspend fun isAuthorized(): Boolean {
val token = getAuthData()?.optJSONObject("token")?.getStringOrNull("access_token")
return !token.isNullOrEmpty()
}
override suspend fun getUsername(): String = getAuthData()
?.getJSONObject("auth")
@ -65,6 +67,18 @@ internal abstract class LibSocialParser(
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(
1, MangaState.ONGOING,
2, MangaState.FINISHED,
@ -374,7 +388,8 @@ internal abstract class LibSocialParser(
}
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 {

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

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

@ -40,7 +40,10 @@ internal object MangaLoaderContextMock : MangaLoaderContext() {
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 {
it.evaluate(script)?.toString()
}

Loading…
Cancel
Save