mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Implemented httpimp.
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
aar/* filter=lfs diff=lfs merge=lfs -text
|
||||
app/aar/* filter=lfs diff=lfs merge=lfs -text
|
||||
app/src/main/jniLibs/arm64-v8a filter=lfs diff=lfs merge=lfs -text
|
||||
app/src/main/jniLibs/armeabi-v7a filter=lfs diff=lfs merge=lfs -text
|
||||
app/src/main/jniLibs/x86 filter=lfs diff=lfs merge=lfs -text
|
||||
app/src/main/jniLibs/x86_64 filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -146,6 +146,7 @@ android {
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||
assets {
|
||||
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
object AppCaUpdater {
|
||||
private const val CA_URL = "https://curl.se/ca/cacert.pem"
|
||||
private const val CACHE_FILENAME = "curl-ca-bundle.pem"
|
||||
private const val MAX_AGE_DAYS = 30
|
||||
|
||||
suspend fun ensureCaBundle(context: Context): File = withContext(Dispatchers.IO) {
|
||||
val file = File(context.noBackupFilesDir, CACHE_FILENAME)
|
||||
val needsUpdate = !file.exists() || isOlderThanDays(file, MAX_AGE_DAYS)
|
||||
if (needsUpdate) {
|
||||
downloadToFile(CA_URL, file)
|
||||
}
|
||||
return@withContext file
|
||||
}
|
||||
|
||||
private fun isOlderThanDays(file: File, days: Int): Boolean {
|
||||
val ageMs = System.currentTimeMillis() - file.lastModified()
|
||||
return ageMs > days * 24L * 60L * 60L * 1000L
|
||||
}
|
||||
|
||||
private fun downloadToFile(urlStr: String, dest: File) {
|
||||
val conn = (URL(urlStr).openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = 15000
|
||||
readTimeout = 15000
|
||||
instanceFollowRedirects = true
|
||||
}
|
||||
conn.inputStream.use { input ->
|
||||
dest.parentFile?.mkdirs()
|
||||
dest.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.withStateAtLeast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.curlbind.Libcurl
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.RootInsetsController
|
||||
@@ -275,6 +276,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
@UnstableApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Logger.w(TAG, "MainActivity Starting [$mainId]");
|
||||
|
||||
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
|
||||
StateApp.instance.mainAppStarting(this);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||
import com.futo.platformplayer.engine.packages.PackageHttpImp
|
||||
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||
import com.futo.platformplayer.engine.packages.V8Package
|
||||
@@ -383,6 +384,7 @@ class V8Plugin {
|
||||
return when(packageName) {
|
||||
"DOMParser" -> PackageDOMParser(this)
|
||||
"Http" -> PackageHttp(this, config)
|
||||
"HttpImp" -> PackageHttpImp(this, config)
|
||||
"Utilities" -> PackageUtilities(this, config)
|
||||
"JSDOM" -> PackageJSDOM(this, config)
|
||||
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.curlbind
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.collections.iterator
|
||||
import kotlin.math.min
|
||||
|
||||
@Keep
|
||||
object Libcurl {
|
||||
init {
|
||||
System.loadLibrary("curl-impersonate")
|
||||
System.loadLibrary("curl-impersonate-jni")
|
||||
// CURL_GLOBAL_ALL = 3
|
||||
require(ce_global_init(3) == CURLcode.CURLE_OK) { "curl_global_init failed" }
|
||||
}
|
||||
|
||||
@Keep
|
||||
data class Request(
|
||||
var url: String,
|
||||
var method: String = "GET",
|
||||
var headers: Map<String, String> = emptyMap(),
|
||||
var body: ByteArray? = null,
|
||||
var impersonateTarget: String = "chrome136",
|
||||
var useBuiltInHeaders: Boolean = true,
|
||||
var timeoutMs: Int = 30_000,
|
||||
var cookieJarPath: String? = null,
|
||||
var sendCookies: Boolean = true,
|
||||
var persistCookies: Boolean = true,
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class Response(
|
||||
val status: Int,
|
||||
val effectiveUrl: String,
|
||||
val bodyBytes: ByteArray,
|
||||
val headers: Map<String, List<String>>
|
||||
)
|
||||
|
||||
object CURLcode {
|
||||
const val CURLE_OK = 0
|
||||
const val CURLE_UNKNOWN_OPTION = 48
|
||||
}
|
||||
|
||||
object CurlInfoConsts {
|
||||
const val CURLINFO_STRING = 0x100000
|
||||
const val CURLINFO_LONG = 0x200000
|
||||
const val CURLINFO_DOUBLE = 0x300000
|
||||
const val CURLINFO_SLIST = 0x400000
|
||||
const val CURLINFO_PTR = 0x400000
|
||||
const val CURLINFO_SOCKET = 0x500000
|
||||
const val CURLINFO_OFF_T = 0x600000
|
||||
const val CURLINFO_MASK = 0x0fffff
|
||||
const val CURLINFO_TYPEMASK = 0xf00000
|
||||
}
|
||||
|
||||
object CURLINFO {
|
||||
const val NONE = 0
|
||||
const val EFFECTIVE_URL = CurlInfoConsts.CURLINFO_STRING + 1
|
||||
const val RESPONSE_CODE = CurlInfoConsts.CURLINFO_LONG + 2
|
||||
}
|
||||
|
||||
object CURLOPT {
|
||||
const val URL = 10002
|
||||
const val FOLLOWLOCATION = 52
|
||||
const val MAXREDIRS = 68
|
||||
const val CONNECTTIMEOUT_MS = 156
|
||||
const val TIMEOUT_MS = 155
|
||||
const val HTTP_VERSION = 84
|
||||
const val ACCEPT_ENCODING = 10102
|
||||
const val HTTPHEADER = 10023
|
||||
const val COOKIEFILE = 10031
|
||||
const val COOKIEJAR = 10082
|
||||
const val CUSTOMREQUEST = 10036
|
||||
const val IPRESOLVE = 113
|
||||
const val POSTFIELDS = 10015
|
||||
const val POSTFIELDSIZE = 60
|
||||
const val WRITEFUNCTION = 20011
|
||||
const val HEADERFUNCTION = 20079
|
||||
const val WRITEDATA = 10001
|
||||
const val HEADERDATA = 10029
|
||||
const val COPYPOSTFIELDS = 10165
|
||||
const val CURLOPT_DNS_SERVERS = 10211
|
||||
const val CAPATH = 10097
|
||||
const val CAINFO = 10065
|
||||
}
|
||||
|
||||
object CURL_HTTP_VERSION { const val TWO_TLS = 4 }
|
||||
object CURL_IPRESOLVE { const val WHATEVER = 0; const val V4 = 1; const val V6 = 2 }
|
||||
|
||||
@Keep interface WriteCallback { fun onWrite(chunk: ByteArray): Int }
|
||||
@Keep interface HeaderCallback { fun onHeader(line: ByteArray): Int }
|
||||
|
||||
@Volatile private var defaultCAPath: String? = null
|
||||
@Keep fun setDefaultCAPath(path: String) { defaultCAPath = path }
|
||||
|
||||
fun perform(req: Request): Response {
|
||||
val easy = ce_easy_init()
|
||||
require(easy != 0L) { "curl_easy_init failed" }
|
||||
|
||||
var slist: Long = 0L
|
||||
val bodySink = ByteArrayOutputStream(64 * 1024)
|
||||
val rawHeaderLines = ArrayList<String>(64)
|
||||
|
||||
try {
|
||||
val imp = ce_easy_impersonate(easy, req.impersonateTarget, req.useBuiltInHeaders)
|
||||
if (imp != CURLcode.CURLE_OK && imp != CURLcode.CURLE_UNKNOWN_OPTION) {
|
||||
error("curl_easy_impersonate failed: ${ce_easy_strerror(imp)}")
|
||||
}
|
||||
|
||||
checkOK(ce_setopt_str(easy, CURLOPT.URL, req.url))
|
||||
checkOK(ce_setopt_long(easy, CURLOPT.FOLLOWLOCATION, 1))
|
||||
checkOK(ce_setopt_long(easy, CURLOPT.MAXREDIRS, 10))
|
||||
checkOK(ce_setopt_long(easy, CURLOPT.CONNECTTIMEOUT_MS, req.timeoutMs.toLong()))
|
||||
checkOK(ce_setopt_long(easy, CURLOPT.TIMEOUT_MS, req.timeoutMs.toLong()))
|
||||
checkOK(ce_setopt_long(easy, CURLOPT.HTTP_VERSION, CURL_HTTP_VERSION.TWO_TLS.toLong()))
|
||||
checkOK(ce_setopt_str(easy, CURLOPT.ACCEPT_ENCODING, "")) // enable auto-decompress
|
||||
|
||||
if (req.headers.isNotEmpty()) {
|
||||
for ((k, v) in req.headers) slist = ce_slist_append(slist, "$k: $v")
|
||||
if (slist != 0L) checkOK(ce_setopt_ptr(easy, CURLOPT.HTTPHEADER, slist))
|
||||
}
|
||||
|
||||
if (req.sendCookies || req.persistCookies) {
|
||||
val jar = (req.cookieJarPath ?: defaultCookieJarPath())
|
||||
if (req.sendCookies) checkOK(ce_setopt_str(easy, CURLOPT.COOKIEFILE, jar))
|
||||
if (req.persistCookies) checkOK(ce_setopt_str(easy, CURLOPT.COOKIEJAR, jar))
|
||||
}
|
||||
|
||||
val method = req.method
|
||||
if (!method.equals("GET", ignoreCase = true)) {
|
||||
checkOK(ce_setopt_str(easy, CURLOPT.CUSTOMREQUEST, method))
|
||||
val body = req.body
|
||||
if (body != null && body.isNotEmpty()) {
|
||||
checkOK(ce_set_postfields(easy, body))
|
||||
}
|
||||
}
|
||||
|
||||
checkOK(ce_set_write_callback(easy, object : WriteCallback {
|
||||
override fun onWrite(chunk: ByteArray): Int {
|
||||
bodySink.write(chunk)
|
||||
return chunk.size
|
||||
}
|
||||
}))
|
||||
checkOK(ce_set_header_callback(easy, object : HeaderCallback {
|
||||
override fun onHeader(line: ByteArray): Int {
|
||||
// Keep raw but trim CRLF for convenience
|
||||
val s = line.toString(Charset.forName("ISO-8859-1")).trimEnd('\r', '\n')
|
||||
if (s.isNotBlank()) rawHeaderLines.add(s)
|
||||
return line.size
|
||||
}
|
||||
}))
|
||||
|
||||
checkOK(ce_setopt_str(easy, CURLOPT.CURLOPT_DNS_SERVERS, "1.1.1.1,8.8.8.8"));
|
||||
defaultCAPath?.let { checkOK(ce_setopt_str(easy, CURLOPT.CAINFO, it)) }
|
||||
|
||||
val rc = ce_easy_perform(easy)
|
||||
if (rc != CURLcode.CURLE_OK) error("curl_easy_perform failed: ${ce_easy_strerror(rc)}")
|
||||
|
||||
val codeArr = longArrayOf(0)
|
||||
checkOK(ce_easy_getinfo_long(easy, CURLINFO.RESPONSE_CODE, codeArr))
|
||||
val effective = ce_easy_getinfo_string(easy, CURLINFO.EFFECTIVE_URL) ?: req.url
|
||||
|
||||
return Response(
|
||||
status = codeArr[0].toInt(),
|
||||
effectiveUrl = effective,
|
||||
bodyBytes = bodySink.toByteArray(),
|
||||
headers = parseHeaders(rawHeaderLines)
|
||||
)
|
||||
} finally {
|
||||
if (slist != 0L) ce_slist_free_all(slist)
|
||||
ce_easy_cleanup(easy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun defaultCookieJarPath(): String {
|
||||
val tmp = System.getProperty("java.io.tmpdir") ?: "/data/local/tmp"
|
||||
return if (tmp.endsWith("/")) "${tmp}imphttp.cookies.txt" else "$tmp/imphttp.cookies.txt"
|
||||
}
|
||||
|
||||
private fun checkOK(code: Int) {
|
||||
if (code != CURLcode.CURLE_OK) throw IllegalStateException("libcurl error: ${ce_easy_strerror(code)}")
|
||||
}
|
||||
|
||||
private fun parseHeaders(lines: List<String>): Map<String, List<String>> {
|
||||
val map = linkedMapOf<String, MutableList<String>>()
|
||||
for (line in lines) {
|
||||
val idx = line.indexOf(':')
|
||||
if (idx <= 0) continue
|
||||
val name = line.substring(0, idx).trim()
|
||||
val value = line.substring(min(idx + 1, line.length)).trim()
|
||||
map.getOrPut(name) { mutableListOf() }.add(value)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
@JvmStatic external fun ce_set_write_callback(easy: Long, cb: WriteCallback?): Int
|
||||
@JvmStatic external fun ce_set_header_callback(easy: Long, cb: HeaderCallback?): Int
|
||||
|
||||
@JvmStatic external fun ce_global_init(flags: Long): Int
|
||||
@JvmStatic external fun ce_global_cleanup()
|
||||
@JvmStatic external fun ce_easy_init(): Long
|
||||
@JvmStatic external fun ce_easy_cleanup(easy: Long)
|
||||
@JvmStatic external fun ce_easy_perform(easy: Long): Int
|
||||
|
||||
@JvmStatic external fun ce_easy_impersonate(easy: Long, target: String, defaultHeaders: Boolean): Int
|
||||
@JvmStatic external fun ce_setopt_long(easy: Long, opt: Int, value: Long): Int
|
||||
@JvmStatic external fun ce_setopt_str(easy: Long, opt: Int, value: String): Int
|
||||
@JvmStatic external fun ce_setopt_ptr(easy: Long, opt: Int, ptr: Long): Int
|
||||
@JvmStatic external fun ce_slist_append(list: Long, header: String): Long
|
||||
@JvmStatic external fun ce_slist_free_all(list: Long)
|
||||
@JvmStatic external fun ce_easy_getinfo_long(easy: Long, info: Int, outVal: LongArray): Int
|
||||
@JvmStatic external fun ce_easy_getinfo_string(easy: Long, info: Int): String?
|
||||
|
||||
@JvmStatic external fun ce_set_postfields(easy: Long, body: ByteArray): Int
|
||||
@JvmStatic external fun ce_easy_strerror(code: Int): String
|
||||
}
|
||||
@@ -0,0 +1,787 @@
|
||||
package com.futo.platformplayer.engine.packages
|
||||
|
||||
import com.caoccao.javet.annotations.V8Convert
|
||||
import com.caoccao.javet.annotations.V8Function
|
||||
import com.caoccao.javet.annotations.V8Property
|
||||
import com.caoccao.javet.enums.V8ConversionMode
|
||||
import com.caoccao.javet.enums.V8ProxyMode
|
||||
import com.caoccao.javet.interop.V8Runtime
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
||||
import com.curlbind.Libcurl
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import java.net.SocketTimeoutException
|
||||
import java.nio.charset.Charset
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
import kotlin.math.min
|
||||
|
||||
class PackageHttpImp : V8Package {
|
||||
@Transient
|
||||
internal val _config: IV8PluginConfig
|
||||
|
||||
@Transient
|
||||
private val _packageClient: PackageHttpClient
|
||||
|
||||
@Transient
|
||||
private val _packageClientAuth: PackageHttpClient
|
||||
|
||||
override val name: String get() = "HttpImp"
|
||||
override val variableName: String get() = "httpimp"
|
||||
|
||||
private var _batchPoolLock: Any = Any()
|
||||
private var _batchPool: ForkJoinPool? = null
|
||||
|
||||
private val _clients = mutableMapOf<String, PackageHttpClient>()
|
||||
|
||||
constructor(plugin: V8Plugin, config: IV8PluginConfig) : super(plugin) {
|
||||
_config = config
|
||||
_packageClient = PackageHttpClient(this, withAuth = false)
|
||||
_packageClientAuth = PackageHttpClient(this, withAuth = true)
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
Logger.w(TAG, "PackageHttpImp Cleaning up")
|
||||
}
|
||||
|
||||
private fun <T, R> autoParallelPool(
|
||||
data: List<T>,
|
||||
parallelism: Int,
|
||||
handle: (T) -> R
|
||||
): List<Pair<R?, Throwable?>> {
|
||||
synchronized(_batchPoolLock) {
|
||||
val threadsToUse = if (parallelism <= 0) data.size else min(parallelism, data.size)
|
||||
if (_batchPool == null) {
|
||||
_batchPool = ForkJoinPool(threadsToUse)
|
||||
}
|
||||
var pool = _batchPool ?: return listOf()
|
||||
if (pool.poolSize < threadsToUse) {
|
||||
pool.shutdown()
|
||||
_batchPool = ForkJoinPool(threadsToUse)
|
||||
pool = _batchPool ?: return listOf()
|
||||
}
|
||||
|
||||
val resultTasks = mutableListOf<ForkJoinTask<Pair<R?, Throwable?>>>()
|
||||
for (item in data) {
|
||||
resultTasks.add(
|
||||
pool.submit<Pair<R?, Throwable?>> {
|
||||
try {
|
||||
Pair(handle(item), null)
|
||||
} catch (ex: Throwable) {
|
||||
Pair<R?, Throwable?>(null, ex)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
return resultTasks.map { it.join() }
|
||||
}
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun newClient(withAuth: Boolean): PackageHttpClient {
|
||||
val client = PackageHttpClient(this, withAuth)
|
||||
client.clientId()?.let { _clients[it] = client }
|
||||
return client
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun getDefaultClient(withAuth: Boolean): PackageHttpClient {
|
||||
return if (withAuth) _packageClientAuth else _packageClient
|
||||
}
|
||||
|
||||
fun getClient(id: String?): PackageHttpClient {
|
||||
if (id == null) throw IllegalArgumentException("Http client $id doesn't exist")
|
||||
if (_packageClient.clientId() == id) return _packageClient
|
||||
if (_packageClientAuth.clientId() == id) return _packageClientAuth
|
||||
return _clients[id] ?: throw IllegalArgumentException("Http client $id doesn't exist")
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun batch(): BatchBuilder {
|
||||
return BatchBuilder(this)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun request(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useAuth: Boolean = false,
|
||||
bytesResult: Boolean = false
|
||||
): IBridgeHttpResponse {
|
||||
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||
return client.requestInternal(
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
if (bytesResult) ReturnType.BYTES else ReturnType.STRING
|
||||
)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun requestWithBody(
|
||||
method: String,
|
||||
url: String,
|
||||
body: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useAuth: Boolean = false,
|
||||
bytesResult: Boolean = false
|
||||
): IBridgeHttpResponse {
|
||||
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||
return client.requestWithBodyInternal(
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
headers,
|
||||
if (bytesResult) ReturnType.BYTES else ReturnType.STRING
|
||||
)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun GET(
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useAuth: Boolean = false,
|
||||
useByteResponse: Boolean = false
|
||||
): IBridgeHttpResponse {
|
||||
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||
return client.GETInternal(
|
||||
url,
|
||||
headers,
|
||||
if (useByteResponse) ReturnType.BYTES else ReturnType.STRING
|
||||
)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun POST(
|
||||
url: String,
|
||||
body: Any,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useAuth: Boolean = false,
|
||||
useByteResponse: Boolean = false
|
||||
): IBridgeHttpResponse {
|
||||
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||
|
||||
return when (body) {
|
||||
is V8ValueString ->
|
||||
client.POSTInternal(url, body.value, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||
is String ->
|
||||
client.POSTInternal(url, body, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||
is V8ValueTypedArray ->
|
||||
client.POSTInternal(url, body.toBytes(), headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||
is ByteArray ->
|
||||
client.POSTInternal(url, body, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||
is ArrayList<*> ->
|
||||
client.POSTInternal(
|
||||
url,
|
||||
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
||||
headers,
|
||||
if (useByteResponse) ReturnType.BYTES else ReturnType.STRING
|
||||
)
|
||||
else -> throw NotImplementedError("Body type ${body?.javaClass?.name} not implemented for POST")
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> logExceptions(handle: () -> T): T {
|
||||
try {
|
||||
return handle()
|
||||
} catch (ex: Exception) {
|
||||
Logger.e("Plugin[${_config.name}]", ex.message, ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
interface IBridgeHttpResponse {
|
||||
val url: String
|
||||
val code: Int
|
||||
val headers: Map<String, List<String>>?
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class BridgeHttpStringResponse(
|
||||
override val url: String,
|
||||
override val code: Int,
|
||||
val body: String?,
|
||||
override val headers: Map<String, List<String>>? = null
|
||||
) : IV8Convertable, IBridgeHttpResponse {
|
||||
val isOk = code in 200..299
|
||||
|
||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||
val obj = runtime.createV8ValueObject()
|
||||
obj.set("url", url)
|
||||
obj.set("code", code)
|
||||
obj.set("body", body)
|
||||
obj.set("headers", headers)
|
||||
obj.set("isOk", isOk)
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class BridgeHttpBytesResponse(
|
||||
override val url: String,
|
||||
override val code: Int,
|
||||
val body: ByteArray? = null,
|
||||
override val headers: Map<String, List<String>>? = null
|
||||
) : IV8Convertable, IBridgeHttpResponse {
|
||||
val isOk: Boolean = code in 200..299
|
||||
|
||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||
val obj = runtime.createV8ValueObject()
|
||||
obj.set("url", url)
|
||||
obj.set("code", code)
|
||||
if (body != null) {
|
||||
obj.set("body", body)
|
||||
}
|
||||
obj.set("headers", headers)
|
||||
obj.set("isOk", isOk)
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||
class BatchBuilder(
|
||||
@Transient private val _package: PackageHttpImp,
|
||||
existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()
|
||||
) : V8BindObject() {
|
||||
@Transient
|
||||
private val _reqs = existingRequests
|
||||
|
||||
@V8Function
|
||||
fun request(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useAuth: Boolean = false
|
||||
): BatchBuilder {
|
||||
return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun requestWithBody(
|
||||
method: String,
|
||||
url: String,
|
||||
body: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useAuth: Boolean = false
|
||||
): BatchBuilder {
|
||||
return clientRequestWithBody(_package.getDefaultClient(useAuth).clientId(), method, url, body, headers)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun GET(
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useAuth: Boolean = false
|
||||
): BatchBuilder =
|
||||
clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers)
|
||||
|
||||
@V8Function
|
||||
fun POST(
|
||||
url: String,
|
||||
body: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useAuth: Boolean = false
|
||||
): BatchBuilder =
|
||||
clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers)
|
||||
|
||||
@V8Function
|
||||
fun DUMMY(): BatchBuilder {
|
||||
_reqs.add(
|
||||
Pair(
|
||||
_package.getDefaultClient(false),
|
||||
RequestDescriptor("DUMMY", "", mutableMapOf())
|
||||
)
|
||||
)
|
||||
return BatchBuilder(_package, _reqs)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun clientRequest(
|
||||
clientId: String?,
|
||||
method: String,
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap()
|
||||
): BatchBuilder {
|
||||
_reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers)))
|
||||
return BatchBuilder(_package, _reqs)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun clientRequestWithBody(
|
||||
clientId: String?,
|
||||
method: String,
|
||||
url: String,
|
||||
body: String,
|
||||
headers: MutableMap<String, String> = HashMap()
|
||||
): BatchBuilder {
|
||||
_reqs.add(
|
||||
Pair(
|
||||
_package.getClient(clientId),
|
||||
RequestDescriptor(method, url, headers, body)
|
||||
)
|
||||
)
|
||||
return BatchBuilder(_package, _reqs)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun clientGET(
|
||||
clientId: String?,
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap()
|
||||
): BatchBuilder =
|
||||
clientRequest(clientId, "GET", url, headers)
|
||||
|
||||
@V8Function
|
||||
fun clientPOST(
|
||||
clientId: String?,
|
||||
url: String,
|
||||
body: String,
|
||||
headers: MutableMap<String, String> = HashMap()
|
||||
): BatchBuilder =
|
||||
clientRequestWithBody(clientId, "POST", url, body, headers)
|
||||
|
||||
@V8Function
|
||||
fun execute(): List<IBridgeHttpResponse?> {
|
||||
return _package.autoParallelPool(_reqs, -1) {
|
||||
if (it.second.method == "DUMMY") {
|
||||
return@autoParallelPool null
|
||||
}
|
||||
if (it.second.body != null) {
|
||||
it.first.requestWithBodyInternal(
|
||||
it.second.method,
|
||||
it.second.url,
|
||||
it.second.body!!,
|
||||
it.second.headers,
|
||||
it.second.respType
|
||||
)
|
||||
} else {
|
||||
it.first.requestInternal(
|
||||
it.second.method,
|
||||
it.second.url,
|
||||
it.second.headers,
|
||||
it.second.respType
|
||||
)
|
||||
}
|
||||
}.map {
|
||||
if (it.second != null) throw it.second!!
|
||||
it.first
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||
class PackageHttpClient : V8BindObject {
|
||||
@Transient
|
||||
private val _package: PackageHttpImp
|
||||
|
||||
@Transient
|
||||
private val _withAuth: Boolean
|
||||
|
||||
val parentConfig: IV8PluginConfig
|
||||
get() = _package._config
|
||||
|
||||
@Transient
|
||||
private val _defaultHeaders = mutableMapOf<String, String>()
|
||||
|
||||
@Transient
|
||||
private val _clientId: String = UUID.randomUUID().toString()
|
||||
|
||||
@Volatile
|
||||
private var timeoutMs: Int = 30_000
|
||||
|
||||
@Volatile
|
||||
private var sendCookies: Boolean = true
|
||||
|
||||
@Volatile
|
||||
private var persistCookies: Boolean = true
|
||||
|
||||
@Volatile
|
||||
private var cookieJarPath: String? = null
|
||||
|
||||
@Volatile
|
||||
private var impersonateTarget: String = "chrome136"
|
||||
|
||||
@Volatile
|
||||
private var useBuiltInHeaders: Boolean = true
|
||||
|
||||
@V8Property
|
||||
fun clientId(): String? = _clientId
|
||||
|
||||
constructor(pack: PackageHttpImp, withAuth: Boolean) : super() {
|
||||
_package = pack
|
||||
_withAuth = withAuth
|
||||
}
|
||||
|
||||
private fun ensureCookieJarPath(): String {
|
||||
val existing = cookieJarPath
|
||||
if (existing != null) return existing
|
||||
|
||||
val tmp = System.getProperty("java.io.tmpdir") ?: "/data/local/tmp"
|
||||
val safeName = parentConfig.name.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||
val fileName =
|
||||
if (_withAuth) "imphttp.$safeName.auth.cookies.txt" else "imphttp.$safeName.cookies.txt"
|
||||
val path = if (tmp.endsWith("/")) tmp + fileName else "$tmp/$fileName"
|
||||
cookieJarPath = path
|
||||
return path
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
|
||||
synchronized(_defaultHeaders) {
|
||||
for (pair in defaultHeaders) {
|
||||
_defaultHeaders[pair.key] = pair.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun setDoApplyCookies(apply: Boolean) {
|
||||
sendCookies = apply
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun setDoUpdateCookies(update: Boolean) {
|
||||
persistCookies = update
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun setDoAllowNewCookies(allow: Boolean) {
|
||||
persistCookies = allow
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun setTimeout(timeoutMs: Int) {
|
||||
this.timeoutMs = timeoutMs
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun request(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useBytes: Boolean = false
|
||||
): IBridgeHttpResponse =
|
||||
requestInternal(method, url, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||
|
||||
fun requestInternal(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
returnType: ReturnType
|
||||
): IBridgeHttpResponse {
|
||||
applyDefaultHeaders(headers)
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val resp = performCurl(method, url, headers, null)
|
||||
responseToBridge(resp, returnType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun requestWithBody(
|
||||
method: String,
|
||||
url: String,
|
||||
body: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useBytes: Boolean = false
|
||||
): IBridgeHttpResponse =
|
||||
requestWithBodyInternal(
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
headers,
|
||||
if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
||||
)
|
||||
|
||||
fun requestWithBodyInternal(
|
||||
method: String,
|
||||
url: String,
|
||||
body: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
returnType: ReturnType
|
||||
): IBridgeHttpResponse {
|
||||
applyDefaultHeaders(headers)
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val resp = performCurl(method, url, headers, body.toByteArray())
|
||||
responseToBridge(resp, returnType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun GET(
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useBytes: Boolean = false
|
||||
): IBridgeHttpResponse =
|
||||
GETInternal(url, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||
|
||||
fun GETInternal(
|
||||
url: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
returnType: ReturnType = ReturnType.STRING
|
||||
): IBridgeHttpResponse {
|
||||
applyDefaultHeaders(headers)
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val resp = performCurl("GET", url, headers, null)
|
||||
responseToBridge(resp, returnType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun POST(
|
||||
url: String,
|
||||
body: Any,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
useBytes: Boolean = false
|
||||
): IBridgeHttpResponse {
|
||||
return when (body) {
|
||||
is V8ValueString ->
|
||||
POSTInternal(url, body.value, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||
is String ->
|
||||
POSTInternal(url, body, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||
is V8ValueTypedArray ->
|
||||
POSTInternal(url, body.toBytes(), headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||
is ByteArray ->
|
||||
POSTInternal(url, body, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||
is ArrayList<*> ->
|
||||
POSTInternal(
|
||||
url,
|
||||
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
||||
headers,
|
||||
if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
||||
)
|
||||
else -> throw NotImplementedError("Body type ${body?.javaClass?.name} not implemented for POST")
|
||||
}
|
||||
}
|
||||
|
||||
fun POSTInternal(
|
||||
url: String,
|
||||
body: String,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
returnType: ReturnType = ReturnType.STRING
|
||||
): IBridgeHttpResponse {
|
||||
applyDefaultHeaders(headers)
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val resp = performCurl("POST", url, headers, body.toByteArray())
|
||||
responseToBridge(resp, returnType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun POSTInternal(
|
||||
url: String,
|
||||
body: ByteArray,
|
||||
headers: MutableMap<String, String> = HashMap(),
|
||||
returnType: ReturnType = ReturnType.STRING
|
||||
): IBridgeHttpResponse {
|
||||
applyDefaultHeaders(headers)
|
||||
return logExceptions {
|
||||
catchHttp {
|
||||
val resp = performCurl("POST", url, headers, body)
|
||||
responseToBridge(resp, returnType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performCurl(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: Map<String, String>,
|
||||
bodyBytes: ByteArray?
|
||||
): Libcurl.Response {
|
||||
val jar = ensureCookieJarPath()
|
||||
|
||||
val req = Libcurl.Request(
|
||||
url = url,
|
||||
method = method,
|
||||
headers = headers,
|
||||
body = bodyBytes,
|
||||
impersonateTarget = impersonateTarget,
|
||||
useBuiltInHeaders = useBuiltInHeaders,
|
||||
timeoutMs = timeoutMs,
|
||||
cookieJarPath = jar,
|
||||
sendCookies = sendCookies,
|
||||
persistCookies = persistCookies
|
||||
)
|
||||
return Libcurl.perform(req)
|
||||
}
|
||||
|
||||
private fun responseToBridge(
|
||||
resp: Libcurl.Response,
|
||||
returnType: ReturnType
|
||||
): IBridgeHttpResponse {
|
||||
val sanitizedHeaders = sanitizeResponseHeaders(resp.headers, shouldWhitelistHeaders())
|
||||
return when (returnType) {
|
||||
ReturnType.STRING -> {
|
||||
val bodyStr = decodeBody(resp)
|
||||
BridgeHttpStringResponse(resp.effectiveUrl, resp.status, bodyStr, sanitizedHeaders)
|
||||
}
|
||||
ReturnType.BYTES -> {
|
||||
BridgeHttpBytesResponse(resp.effectiveUrl, resp.status, resp.bodyBytes, sanitizedHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeBody(resp: Libcurl.Response): String {
|
||||
if (resp.bodyBytes.isEmpty()) return ""
|
||||
|
||||
val contentTypeHeader = resp.headers.entries.firstOrNull {
|
||||
it.key.equals("content-type", ignoreCase = true)
|
||||
}?.value?.firstOrNull()
|
||||
|
||||
val charset: Charset = contentTypeHeader
|
||||
?.let { parseCharset(it) }
|
||||
?: Charsets.UTF_8
|
||||
|
||||
return String(resp.bodyBytes, charset)
|
||||
}
|
||||
|
||||
private fun parseCharset(contentType: String): Charset? {
|
||||
val parts = contentType.split(";")
|
||||
for (part in parts) {
|
||||
val trimmed = part.trim()
|
||||
val lower = trimmed.lowercase()
|
||||
if (lower.startsWith("charset=")) {
|
||||
val value = trimmed.substringAfter("=", "").trim().trim('"', '\'')
|
||||
return try {
|
||||
Charset.forName(value)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun shouldWhitelistHeaders(): Boolean {
|
||||
val cfg = parentConfig
|
||||
return !(cfg is SourcePluginConfig && cfg.allowAllHttpHeaderAccess)
|
||||
}
|
||||
|
||||
private fun applyDefaultHeaders(headerMap: MutableMap<String, String>) {
|
||||
synchronized(_defaultHeaders) {
|
||||
for (toApply in _defaultHeaders) {
|
||||
if (!headerMap.containsKey(toApply.key)) {
|
||||
headerMap[toApply.key] = toApply.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitizeResponseHeaders(
|
||||
headers: Map<String, List<String>>?,
|
||||
onlyWhitelisted: Boolean = false
|
||||
): Map<String, List<String>> {
|
||||
val result = mutableMapOf<String, List<String>>()
|
||||
if (onlyWhitelisted) {
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
||||
result[lowerCaseHeader] = values
|
||||
}
|
||||
}
|
||||
} else {
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if (lowerCaseHeader == "set-cookie" &&
|
||||
!values.any { it.lowercase().contains("httponly") }
|
||||
) {
|
||||
result[lowerCaseHeader] = values
|
||||
} else {
|
||||
result[lowerCaseHeader] = values
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun logRequest(
|
||||
method: String,
|
||||
url: String,
|
||||
headers: Map<String, String> = HashMap(),
|
||||
body: String?
|
||||
) {
|
||||
Logger.v(TAG) {
|
||||
val sb = StringBuilder()
|
||||
sb.appendLine("HTTP request (libcurl)")
|
||||
sb.appendLine("$method $url")
|
||||
for (pair in headers) {
|
||||
sb.appendLine("${pair.key}: ${pair.value}")
|
||||
}
|
||||
if (body != null) {
|
||||
sb.appendLine()
|
||||
sb.appendLine(body)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> logExceptions(handle: () -> T): T {
|
||||
try {
|
||||
return handle()
|
||||
} catch (ex: Exception) {
|
||||
Logger.e("Plugin[${_package._config.name}]", ex.message, ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
private fun catchHttp(handle: () -> IBridgeHttpResponse): IBridgeHttpResponse {
|
||||
return try {
|
||||
handle()
|
||||
} catch (ex: SocketTimeoutException) {
|
||||
BridgeHttpStringResponse("", 408, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class RequestDescriptor(
|
||||
val method: String,
|
||||
val url: String,
|
||||
val headers: MutableMap<String, String>,
|
||||
val body: String? = null,
|
||||
val contentType: String? = null,
|
||||
val respType: ReturnType = ReturnType.STRING
|
||||
)
|
||||
|
||||
private fun catchHttp(handle: () -> BridgeHttpStringResponse): BridgeHttpStringResponse {
|
||||
return try {
|
||||
handle()
|
||||
} catch (ex: SocketTimeoutException) {
|
||||
BridgeHttpStringResponse("", 408, null)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ReturnType(val value: Int) {
|
||||
STRING(0),
|
||||
BYTES(1);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PackageHttpImp"
|
||||
private val WHITELISTED_RESPONSE_HEADERS = listOf(
|
||||
"content-type",
|
||||
"date",
|
||||
"content-length",
|
||||
"last-modified",
|
||||
"etag",
|
||||
"cache-control",
|
||||
"content-encoding",
|
||||
"content-disposition",
|
||||
"connection"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.*
|
||||
import com.curlbind.Libcurl
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs.Action
|
||||
@@ -382,6 +383,16 @@ class StateApp {
|
||||
Logger.i(TAG, "MainApp Starting");
|
||||
initializeFiles(true);
|
||||
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val caFile = AppCaUpdater.ensureCaBundle(context)
|
||||
Libcurl.setDefaultCAPath(caFile.absolutePath)
|
||||
} catch (t: Throwable) {
|
||||
val fallback = File(context.noBackupFilesDir, "curl-ca-bundle.pem")
|
||||
if (fallback.exists()) Libcurl.setDefaultCAPath(fallback.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
if(Settings.instance.other.polycentricLocalCache) {
|
||||
Logger.i(TAG, "Initialize Polycentric Disk Cache")
|
||||
_cacheDirectory?.let { ApiMethods.initCache(it) };
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Submodule app/src/stable/assets/sources/kick updated: 4ff0b02700...9b3c7ea213
Submodule app/src/unstable/assets/sources/kick updated: 4ff0b02700...9b3c7ea213
Reference in New Issue
Block a user