[LibSocial] Authorization support
This commit is contained in:
@@ -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,12 +35,11 @@ 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getUsername(): String {
|
override suspend fun getUsername(): String {
|
||||||
val body = webClient.httpGet("https://${domain}/account/profiles").parseHtml().body()
|
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 availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain(
|
get() {
|
||||||
if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED,
|
val isAuthorized = checkAuth()
|
||||||
if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED,
|
return ConfigKey.Domain(
|
||||||
)
|
if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED,
|
||||||
|
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,12 +49,11 @@ 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getUsername(): String {
|
override suspend fun getUsername(): String {
|
||||||
val body = webClient.httpGet("https://${domain}/user/profile").parseHtml().body()
|
val body = webClient.httpGet("https://${domain}/user/profile").parseHtml().body()
|
||||||
|
|||||||
@@ -36,12 +36,11 @@ 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// It will be easier to connect to a manga page, as the source redirects to a lot of advertising.
|
// It will be easier to connect to a manga page, as the source redirects to a lot of advertising.
|
||||||
override suspend fun getUsername(): String {
|
override suspend fun getUsername(): String {
|
||||||
|
|||||||
@@ -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,8 +180,10 @@ 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 =
|
||||||
?: "No name"
|
element.selectFirst("div.text-ellipsis.truncate.text-white.text-center.text-lg.z-20.w-\\[90\\%\\]")
|
||||||
|
?.text()
|
||||||
|
?: "No name"
|
||||||
Manga(
|
Manga(
|
||||||
id = generateUid(mangaId),
|
id = generateUid(mangaId),
|
||||||
url = mangaId,
|
url = mangaId,
|
||||||
|
|||||||
@@ -45,10 +45,9 @@ 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"
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getUsername(): String {
|
override suspend fun getUsername(): String {
|
||||||
val body = webClient.httpGet("https://${getDomain("app")}/my/apps").parseHtml().body()
|
val body = webClient.httpGet("https://${getDomain("app")}/my/apps").parseHtml().body()
|
||||||
|
|||||||
@@ -82,12 +82,11 @@ 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getUsername(): String {
|
override suspend fun getUsername(): String {
|
||||||
val body = webClient.httpGet("https://${domain}/").parseHtml().body()
|
val body = webClient.httpGet("https://${domain}/").parseHtml().body()
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ 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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
SortOrder.NEWEST,
|
SortOrder.NEWEST,
|
||||||
@@ -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,12 +50,11 @@ 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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val regexLastUrlPath = Regex("/[^/]+/?$")
|
private val regexLastUrlPath = Regex("/[^/]+/?$")
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -511,7 +510,7 @@ internal abstract class GroupleParser(
|
|||||||
id = generateUid(url),
|
id = generateUid(url),
|
||||||
url = if (fullUrl.contains("one-way.work")) {
|
url = if (fullUrl.contains("one-way.work")) {
|
||||||
// domain that does not need a token
|
// domain that does not need a token
|
||||||
fullUrl.substringBefore("?")
|
fullUrl.substringBefore("?")
|
||||||
} else {
|
} else {
|
||||||
fullUrl
|
fullUrl
|
||||||
},
|
},
|
||||||
@@ -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,10 +33,10 @@ 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()
|
||||||
?.getJSONObject("auth")
|
?.getJSONObject("auth")
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user