[LibSocial] Authorization support
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user