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
79 changes: 54 additions & 25 deletions app/src/main/java/be/mygod/reactmap/webkit/ReactMapFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ import org.json.JSONObject
import org.json.JSONTokener
import timber.log.Timber
import java.io.IOException
import java.io.InputStream
import java.io.Reader
import java.io.StringWriter
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLDecoder
import java.nio.charset.Charset
import java.util.Locale
Expand All @@ -57,6 +60,18 @@ class ReactMapFragment : Fragment() {
private val mapHijacker = "(?<=[\\n\\r\\s,])this(?=.callInitHooks\\(\\)[,;][\\n\\r\\s]*this._zoomAnimated\\s*=)"
.toPattern()
private val supportedHosts = setOf("discordapp.com", "discord.com", "telegram.org", "oauth.telegram.org")
private val mediaExtensions = setOf(
"apng", "png", "avif", "gif", "jpg", "jpeg", "jfif", "pjpeg", "pjp", "png", "svg", "webp", "bmp", "ico", "cur",
"wav", "mp3", "mp4", "aac", "ogg", "flac",
"css", "js",
"ttf", "otf", "woff", "woff2",
)
private val mediaAcceptMatcher = "image/.*|text/css(?:[,;].*)?".toRegex(RegexOption.IGNORE_CASE)

private val newWebResourceResponse by lazy {
WebResourceResponse::class.java.getDeclaredConstructor(Boolean::class.java, String::class.java,
String::class.java, Int::class.java, String::class.java, Map::class.java, InputStream::class.java)
}
}

private lateinit var web: WebView
Expand Down Expand Up @@ -168,17 +183,28 @@ class ReactMapFragment : Fragment() {
}
}

override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest) =
if (!"https".equals(request.url.scheme, true) || request.url.host != hostname) null
else when (val path = request.url.path) {
// Since CookieManager.getCookie does not return session cookie on main requests,
// we can only edit secondary files
"/api/settings" -> handleSettings(request)
null -> null
else -> if (path.startsWith("/locales/") && path.endsWith("/translation.json")) {
handleTranslation(request)
} else if (vendorJsMatcher.matchEntire(path) != null) handleVendorJs(request) else null
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest): WebResourceResponse? {
val path = request.url.path ?: return null
if ("https".equals(request.url.scheme, true) && request.url.host == hostname) {
if (path == "/api/settings") return handleSettings(request)
if (path.startsWith("/locales/") && path.endsWith("/translation.json")) {
return handleTranslation(request)
}
if (vendorJsMatcher.matchEntire(path) != null) return handleVendorJs(request)
}
if (ReactMapHttpEngine.isCronet && (path.substringAfterLast('.')
.lowercase(Locale.ENGLISH) in mediaExtensions || request.requestHeaders.any { (key, value) ->
"Accept".equals(key, true) && mediaAcceptMatcher.matches(value)
})) try {
val conn = ReactMapHttpEngine.engine.openConnection(URL(
request.url.toString())) as HttpURLConnection
setupConnection(request, conn)
return createResponse(conn) { conn.findErrorStream }
} catch (e: IOException) {
Timber.d(e)
}
return null
}

override fun onReceivedError(view: WebView?, request: WebResourceRequest, error: WebResourceError) {
if (!request.isForMainFrame) return
Expand Down Expand Up @@ -225,24 +251,27 @@ class ReactMapFragment : Fragment() {
web.loadUrl(activeUrl)
}

private fun buildResponse(request: WebResourceRequest, transform: (Reader) -> String) = try {
val url = request.url.toString()
val conn = ReactMapHttpEngine.connectWithCookie(url) { conn ->
conn.requestMethod = request.method
for ((key, value) in request.requestHeaders) conn.addRequestProperty(key, value)
}
private fun setupConnection(request: WebResourceRequest, conn: HttpURLConnection) {
conn.requestMethod = request.method
for ((key, value) in request.requestHeaders) conn.addRequestProperty(key, value)
}
private fun createResponse(conn: HttpURLConnection, data: (Charset) -> InputStream): WebResourceResponse {
val charset = if (conn.contentEncoding == null) Charsets.UTF_8 else {
Charset.forName(conn.contentEncoding)
}
WebResourceResponse(conn.contentType?.substringBefore(';'), conn.contentEncoding, conn.responseCode,
conn.responseMessage.let { if (it.isNullOrBlank()) "N/A" else it },
conn.headerFields.mapValues { (_, value) -> value.joinToString() },
if (conn.responseCode in 200..299) try {
transform(conn.inputStream.bufferedReader(charset)).byteInputStream(charset)
} catch (e: IOException) {
Timber.d(e)
conn.inputStream.bufferedReader(charset).readText().byteInputStream(charset)
} else conn.findErrorStream.bufferedReader(charset).readText().byteInputStream(charset))
return newWebResourceResponse.newInstance(false, conn.contentType?.substringBefore(';'), conn.contentEncoding,
conn.responseCode, conn.responseMessage, conn.headerFields.mapValues { (_, value) -> value.joinToString() },
data(charset))
}
private fun buildResponse(request: WebResourceRequest, transform: (Reader) -> String) = try {
val url = request.url.toString()
val conn = ReactMapHttpEngine.connectWithCookie(url) { conn -> setupConnection(request, conn) }
createResponse(conn) { charset -> if (conn.responseCode in 200..299) try {
transform(conn.inputStream.bufferedReader(charset)).byteInputStream(charset)
} catch (e: IOException) {
Timber.d(e)
conn.inputStream.bufferedReader(charset).readText().byteInputStream(charset)
} else conn.findErrorStream.bufferedReader(charset).readText().byteInputStream(charset) }
} catch (e: IOException) {
Timber.d(e)
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import kotlin.coroutines.resumeWithException
object ReactMapHttpEngine {
private const val KEY_COOKIE = "cookie.graphql"

val isCronet get() = Build.VERSION.SDK_INT >= 34 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7
@get:RequiresExtension(Build.VERSION_CODES.S, 7)
private val engine by lazy @RequiresExtension(Build.VERSION_CODES.S, 7) {
val engine by lazy @RequiresExtension(Build.VERSION_CODES.S, 7) {
val cache = File(app.deviceStorage.cacheDir, "httpEngine")
HttpEngine.Builder(app.deviceStorage).apply {
if (cache.mkdirs() || cache.isDirectory) {
setStoragePath(cache.absolutePath)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK, 1024 * 1024)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK, 512 * 1024 * 1024)
}
setConnectionMigrationOptions(ConnectionMigrationOptions.Builder().apply {
setDefaultNetworkMigration(ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED)
Expand All @@ -43,8 +45,7 @@ object ReactMapHttpEngine {
path("/graphql")
}.build().toString()

private fun openConnection(url: String) = (if (Build.VERSION.SDK_INT >= 34 || Build.VERSION.SDK_INT >=
Build.VERSION_CODES.S && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7) {
private fun openConnection(url: String) = (if (isCronet) {
engine.openConnection(URL(url))
} else URL(url).openConnection()) as HttpURLConnection
suspend fun <T> connectCancellable(url: String, block: suspend (HttpURLConnection) -> T): T {
Expand Down