Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ class PreferencesManager @Inject constructor(
?: io.aatricks.novelscraper.data.model.ReaderTheme.DARK.name
set(value) = prefs.edit().putString(KEY_READER_THEME, value).apply()

var ignoreSslErrors: Boolean
get() = prefs.getBoolean(KEY_IGNORE_SSL_ERRORS, false)
set(value) = prefs.edit().putBoolean(KEY_IGNORE_SSL_ERRORS, value).apply()

// Clear all preferences
fun clearAll() {
prefs.edit().clear().apply()
Expand Down Expand Up @@ -154,5 +158,6 @@ class PreferencesManager @Inject constructor(
private const val KEY_MARGINS = "reader_margins"
private const val KEY_PARAGRAPH_SPACING = "reader_paragraph_spacing"
private const val KEY_READER_THEME = "reader_theme"
private const val KEY_IGNORE_SSL_ERRORS = "ignore_ssl_errors"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.aatricks.novelscraper.data.model

enum class SortMode {
LAST_READ,
DATE_ADDED,
TITLE,
PROGRESS
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import javax.inject.Singleton
@Singleton
class ContentRepository @Inject constructor(
@ApplicationContext private val context: android.content.Context,
private val htmlParser: HtmlParser
private val htmlParser: HtmlParser,
private val okHttpClient: OkHttpClient
) {

companion object {
Expand All @@ -45,11 +46,6 @@ class ContentRepository @Inject constructor(

private val repositoryScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()

private val cacheDir: File get() = File(context.cacheDir, "html_cache").apply { if (!exists()) mkdirs() }
private val mediaCacheDir: File get() = File(context.cacheDir, "media_cache").apply { if (!exists()) mkdirs() }
private val epubCacheDir: File get() = File(context.cacheDir, "epub_cache").apply { if (!exists()) mkdirs() }
Expand Down Expand Up @@ -100,8 +96,12 @@ class ContentRepository @Inject constructor(
}

private fun getReferer(url: String): String = try {
val uri = java.net.URI(url)
"${uri.scheme}://${uri.host}/"
if (url.contains("mangabat") || url.contains("manganato")) {
"https://manganato.com/"
} else {
val uri = java.net.URI(url)
"${uri.scheme}://${uri.host}/"
}
} catch (e: Exception) {
url
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,96 @@
package io.aatricks.novelscraper.data.repository.source

import io.aatricks.novelscraper.data.local.PreferencesManager
import io.aatricks.novelscraper.data.model.ExploreItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Connection
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager

abstract class BaseJsoupSource : NovelSource {
abstract class BaseJsoupSource(
protected open val preferencesManager: PreferencesManager? = null,
protected open val okHttpClient: okhttp3.OkHttpClient? = null
) : NovelSource {
protected open val userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
protected open val timeout = 15000
protected open val timeout = 15000L

protected suspend fun <T> io(block: suspend () -> T): T = withContext(Dispatchers.IO) {
block()
}

protected fun connect(url: String): Connection = Jsoup.connect(url)
.userAgent(userAgent)
.referrer(baseUrl)
.timeout(timeout)
.followRedirects(true)
protected fun getDocument(url: String): Document {
val client = okHttpClient
if (client != null) {
val request = okhttp3.Request.Builder()
.url(url)
.header("User-Agent", userAgent)
.header("Referer", baseUrl)
.build()

client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val html = response.body?.string() ?: throw java.io.IOException("Empty response")
return Jsoup.parse(html, url)
}
}

// Fallback to Jsoup's connection if okHttpClient is not available
val connection = Jsoup.connect(url)
.userAgent(userAgent)
.referrer(baseUrl)
.timeout(timeout.toInt())
.followRedirects(true)

if (preferencesManager?.ignoreSslErrors == true) {
val trustAllCerts = arrayOf<TrustManager>(
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
connection.sslSocketFactory(sslContext.socketFactory)
// Note: setDefaultHostnameVerifier is global and might affect other parts of the app
javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier { _, _ -> true }
}

return connection.get()
}

protected fun getDocument(url: String): Document = connect(url).get()
protected fun connect(url: String): Connection {
// This method is now legacy as we prefer getDocument with okHttpClient
val connection = Jsoup.connect(url)
.userAgent(userAgent)
.referrer(baseUrl)
.timeout(timeout.toInt())
.followRedirects(true)

if (preferencesManager?.ignoreSslErrors == true) {
val trustAllCerts = arrayOf<TrustManager>(
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
connection.sslSocketFactory(sslContext.socketFactory)
javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier { _, _ -> true }
}

return connection
}

protected fun Element.absoluteUrl(attributeKey: String): String {
return resolveUrl(attr(attributeKey))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package io.aatricks.novelscraper.data.repository.source

import io.aatricks.novelscraper.data.local.PreferencesManager
import io.aatricks.novelscraper.data.model.ExploreItem
import java.net.URLEncoder
import javax.inject.Inject

class MangaBatSource : BaseJsoupSource() {
class MangaBatSource @Inject constructor(
override val preferencesManager: PreferencesManager,
override val okHttpClient: okhttp3.OkHttpClient
) : BaseJsoupSource(preferencesManager, okHttpClient) {
override val name = "MangaBat"
override val baseUrl = "https://www.mangabats.com"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.aatricks.novelscraper.data.repository.source

import io.aatricks.novelscraper.data.local.PreferencesManager
import io.aatricks.novelscraper.data.model.ExploreItem
import io.aatricks.novelscraper.data.model.ChapterInfo
import kotlinx.coroutines.Dispatchers
Expand All @@ -9,8 +10,12 @@ import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.jsoup.Jsoup
import java.net.URLEncoder
import javax.inject.Inject

class NovelFireSource : BaseJsoupSource() {
class NovelFireSource @Inject constructor(
override val preferencesManager: PreferencesManager,
override val okHttpClient: okhttp3.OkHttpClient
) : BaseJsoupSource(preferencesManager, okHttpClient) {
override val name = "NovelFire"
override val baseUrl = "https://novelfire.net"

Expand Down
73 changes: 69 additions & 4 deletions app/src/main/java/io/aatricks/novelscraper/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import io.aatricks.novelscraper.data.local.PreferencesManager
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.inject.Singleton
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager

@Module
@InstallIn(SingletonComponent::class)
Expand All @@ -25,12 +31,71 @@ object NetworkModule {

@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
fun provideOkHttpClient(preferencesManager: PreferencesManager): OkHttpClient {
val builder = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
level = HttpLoggingInterceptor.Level.HEADERS // Reduced verbosity for performance
})
.build()
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.followSslRedirects(true)
.followRedirects(true)

try {
val trustManagerFactory = javax.net.ssl.TrustManagerFactory.getInstance(
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()
)
trustManagerFactory.init(null as java.security.KeyStore?)
val defaultTrustManager = trustManagerFactory.trustManagers.first { tm -> tm is X509TrustManager } as X509TrustManager

val dynamicTrustManager = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
if (!preferencesManager.ignoreSslErrors) {
defaultTrustManager.checkClientTrusted(chain, authType)
}
}

override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
if (preferencesManager.ignoreSslErrors) return

try {
defaultTrustManager.checkServerTrusted(chain, authType)
} catch (e: Exception) {
// Double check in case of race condition or update
if (!preferencesManager.ignoreSslErrors) throw e
}
}

override fun getAcceptedIssuers(): Array<X509Certificate> = defaultTrustManager.getAcceptedIssuers()
}

val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(dynamicTrustManager), SecureRandom())

builder.sslSocketFactory(sslContext.socketFactory, dynamicTrustManager)

builder.hostnameVerifier { hostname, session ->
if (preferencesManager.ignoreSslErrors) true
else okhttp3.internal.tls.OkHostnameVerifier.verify(hostname, session)
}
} catch (e: Exception) {
// Fallback to a basic trust-all if something fails during setup and user wants to ignore errors
if (preferencesManager.ignoreSslErrors) {
try {
val trustAllCerts = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(trustAllCerts), SecureRandom())
builder.sslSocketFactory(sslContext.socketFactory, trustAllCerts)
builder.hostnameVerifier { _, _ -> true }
} catch (inner: Exception) {}
}
}

return builder.build()
}

@Provides
Expand Down
11 changes: 9 additions & 2 deletions app/src/main/java/io/aatricks/novelscraper/di/SourceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import io.aatricks.novelscraper.data.local.PreferencesManager
import io.aatricks.novelscraper.data.repository.source.MangaBatSource
import io.aatricks.novelscraper.data.repository.source.NovelFireSource
import io.aatricks.novelscraper.data.repository.source.NovelSource
Expand All @@ -16,10 +17,16 @@ object SourceModule {
@Provides
@Singleton
@IntoSet
fun provideNovelFireSource(): NovelSource = NovelFireSource()
fun provideNovelFireSource(
preferencesManager: PreferencesManager,
okHttpClient: okhttp3.OkHttpClient
): NovelSource = NovelFireSource(preferencesManager, okHttpClient)

@Provides
@Singleton
@IntoSet
fun provideMangaBatSource(): NovelSource = MangaBatSource()
fun provideMangaBatSource(
preferencesManager: PreferencesManager,
okHttpClient: okhttp3.OkHttpClient
): NovelSource = MangaBatSource(preferencesManager, okHttpClient)
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@ fun ReaderImageView(

val imageRequest = remember(imageUrl, pageUrl) {
val uri = try { java.net.URI(pageUrl) } catch (e: Exception) { null }
val referer = if (uri != null) "${uri.scheme}://${uri.host}/" else pageUrl
var referer = if (uri != null) "${uri.scheme}://${uri.host}/" else pageUrl

// Special handling for MangaBat/Manganato images which often require specific referers
if (referer.contains("mangabat") || referer.contains("manganato")) {
referer = "https://manganato.com/"
}

val isCached = cachedFile.exists()

ImageRequest.Builder(context)
Expand Down
Loading
Loading