From bcee0d0c9aba7571e0353193d11c6f7c75cf43fe Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Sun, 26 Oct 2025 16:06:47 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha8=20-=20http=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E8=AF=B7=E6=B1=82=E7=9B=B8=E5=85=B3=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20options.client=20=E9=80=89=E9=A1=B9,=20?= =?UTF-8?q?=E7=94=A8=E4=BA=8E=E9=85=8D=E7=BD=AE=20OkHttpClient.Builder=20(?= =?UTF-8?q?=E5=A6=82=20followRedirects=20=E7=AD=89)=20(issue=20#454)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_zh-Hans.json | 1 + .../autojs/autojs/core/http/MutableOkHttp.kt | 55 +-------- .../autojs/runtime/api/augment/http/Http.kt | 112 +++++++++++++++++- version.properties | 6 +- 4 files changed, 117 insertions(+), 57 deletions(-) diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index cda7f431..e46a3fd6 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -15,6 +15,7 @@ "http 模块请求相关方法获取的 body 对象增加 stream/saveToFile/close 方法 _[`issue #452`](http://issues.autojs6.com/452)_", "http 模块请求相关方法支持缓存控制选项参数 (cacheBody/bodyCacheThresholdBytes)", "http 模块请求相关方法支持不安全选项参数 (isInsecure/insecure), 用于忽略证书相关异常 _[`issue #417`](http://issues.autojs6.com/417)_", + "http 模块请求相关方法支持 options.client 选项, 用于配置 OkHttpClient.Builder (如 followRedirects 等) _[`issue #454`](http://issues.autojs6.com/454)_", "structuredClone 全局方法, 用于深拷贝 JavaScript 对象 (参阅 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/structuredClone))", "设置页面支持应用启动器图标设置选项 _[`issue #405`](http://issues.autojs6.com/405)_", "Scrapers 工具 (run-scrapers.mjs) 用于自动更新 Gradle 构建脚本结构化数据/README 通用数据/README 模板数据等" diff --git a/app/src/main/java/org/autojs/autojs/core/http/MutableOkHttp.kt b/app/src/main/java/org/autojs/autojs/core/http/MutableOkHttp.kt index 97792a1d..6e1f56c9 100644 --- a/app/src/main/java/org/autojs/autojs/core/http/MutableOkHttp.kt +++ b/app/src/main/java/org/autojs/autojs/core/http/MutableOkHttp.kt @@ -1,15 +1,8 @@ package org.autojs.autojs.core.http -import android.annotation.SuppressLint import android.util.Log import okhttp3.Interceptor import okhttp3.OkHttpClient -import java.security.SecureRandom -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager /** * Created by Stardust on Apr 11, 2018. @@ -19,65 +12,25 @@ class MutableOkHttp : OkHttpClient() { private var mTimeout = DEFAULT_TIMEOUT private var mIsInsecure = DEFAULT_IS_INSECURE - private var mOkHttpClient: OkHttpClient + private lateinit var mOkHttpClient: OkHttpClient private val mInterceptors: MutableList = ArrayList() @JvmField var maxRetries = DEFAULT_MAX_RETRIES - var timeout: Long - get() = mTimeout - set(timeout) { - mTimeout = timeout - muteClient() - } - - var isInsecure: Boolean - get() = mIsInsecure - set(isInsecure) { - mIsInsecure = isInsecure - muteClient() - } - init { - mOkHttpClient = newClient(Builder()) mInterceptors.add(getRetryInterceptor()) + muteClient(Builder()) } fun client() = mOkHttpClient - private fun newClient(builder: Builder): OkHttpClient { - mInterceptors.forEach { if (it !in builder.interceptors()) builder.addInterceptor(it) } - return builder - .readTimeout(timeout, TimeUnit.MILLISECONDS) - .writeTimeout(timeout, TimeUnit.MILLISECONDS) - .connectTimeout(timeout, TimeUnit.MILLISECONDS) - .also { b -> - if (!isInsecure) return@also - @SuppressLint("CustomX509TrustManager") - val trustAllCerts = arrayOf(object : X509TrustManager { - override fun checkClientTrusted(chain: Array, authType: String) = Unit - override fun checkServerTrusted(chain: Array, authType: String) = Unit - override fun getAcceptedIssuers(): Array = arrayOf() - }) - val sslContext = SSLContext.getInstance("TLS").apply { - init(null, trustAllCerts, SecureRandom()) - } - val trustManager = trustAllCerts.first() as X509TrustManager - b.sslSocketFactory(sslContext.socketFactory, trustManager) - b.hostnameVerifier { _, _ -> true } - } - .build() - } - @Synchronized fun muteClient(builder: Builder) { - mOkHttpClient = newClient(builder) + mInterceptors.forEach { if (it !in builder.interceptors()) builder.addInterceptor(it) } + mOkHttpClient = builder.build() } - @Synchronized - private fun muteClient() = muteClient(mOkHttpClient.newBuilder()) - private fun getRetryInterceptor() = Interceptor { chain -> // @Reference to stackoverflow.com by SuperMonster003 on Apr 9, 2024. diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/http/Http.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/http/Http.kt index d541f91f..da86c516 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/http/Http.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/http/Http.kt @@ -2,6 +2,7 @@ package org.autojs.autojs.runtime.api.augment.http +import android.annotation.SuppressLint import android.webkit.MimeTypeMap import okhttp3.Call import okhttp3.Callback @@ -9,6 +10,7 @@ import okhttp3.FormBody import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody @@ -47,6 +49,7 @@ import org.autojs.autojs.util.RhinoUtils.UNDEFINED import org.autojs.autojs.util.RhinoUtils.coerceBoolean import org.autojs.autojs.util.RhinoUtils.coerceIntNumber import org.autojs.autojs.util.RhinoUtils.coerceLongNumber +import org.autojs.autojs.util.RhinoUtils.coerceNumber import org.autojs.autojs.util.RhinoUtils.coerceObject import org.autojs.autojs.util.RhinoUtils.coerceString import org.autojs.autojs.util.RhinoUtils.isUiThread @@ -67,7 +70,14 @@ import org.mozilla.javascript.Undefined import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.lang.reflect.Method import java.net.URI +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager @Suppress("unused", "UNUSED_PARAMETER") class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) { @@ -90,6 +100,7 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) { private const val METHOD_GET = "GET" private const val METHOD_POST = "POST" + private const val KEY_CLIENT = "client" private const val KEY_METHOD = "method" private const val KEY_CONTENT_TYPE = "contentType" private const val KEY_HEADERS = "headers" @@ -148,9 +159,10 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) { val opt = coerceObject(options, newNativeObject()) - scriptRuntime.http.okhttp.timeout = coerceLongNumber(opt.prop(KEY_TIMEOUT), DEFAULT_TIMEOUT) - scriptRuntime.http.okhttp.maxRetries = coerceIntNumber(opt.prop(KEY_MAX_RETRIES), DEFAULT_MAX_RETRIES) - scriptRuntime.http.okhttp.isInsecure = opt.inquire(listOf("isInsecure", "insecure"), ::coerceBoolean, false) + scriptRuntime.http.okhttp.apply { + maxRetries = coerceIntNumber(opt.prop(KEY_MAX_RETRIES), DEFAULT_MAX_RETRIES) + applyOkHttpClientBuilder(opt) + } val cacheBody = opt.inquire(listOf(KEY_CACHE_BODY), ::coerceBoolean, DEFAULT_CACHE_BODY) val cacheThreshold = coerceLongNumber(opt.prop(KEY_BODY_CACHE_THRESHOLD_BYTES), DEFAULT_BODY_CACHE_THRESHOLD_BYTES) @@ -276,6 +288,100 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) { } } + private fun MutableOkHttp.applyOkHttpClientBuilder(opt: NativeObject) { + val clientProp = opt.prop(KEY_CLIENT) + require(clientProp is NativeObject?) { "Argument \"client\" ${clientProp.jsBrief()} for http.request must be a JavaScript Object" } + clientProp ?: return + + val timeout = coerceLongNumber(opt.prop(KEY_TIMEOUT), DEFAULT_TIMEOUT) + val isInsecure = opt.inquire(listOf("isInsecure", "insecure"), ::coerceBoolean, false) + + val builder = this.client().newBuilder() + .readTimeout(timeout, TimeUnit.MILLISECONDS) + .writeTimeout(timeout, TimeUnit.MILLISECONDS) + .connectTimeout(timeout, TimeUnit.MILLISECONDS) + + val builderClass = OkHttpClient.Builder::class.java + + fun coerceArg(paramType: Class<*>, value: Any?) = when { + paramType == java.lang.Boolean.TYPE || paramType == java.lang.Boolean::class.java -> coerceBoolean(value, false) + paramType == java.lang.Long.TYPE || paramType == java.lang.Long::class.java -> coerceLongNumber(value, 0L) + paramType == Integer.TYPE || paramType == Integer::class.java -> coerceIntNumber(value, 0) + paramType == java.lang.Double.TYPE || paramType == java.lang.Double::class.java -> coerceNumber(value, 0.0) + paramType == String::class.java -> coerceString(value, "") + paramType == TimeUnit::class.java -> when (value) { + is TimeUnit -> value + is String -> runCatching { TimeUnit.valueOf(value.trim().uppercase()) }.getOrElse { + throw WrappedIllegalArgumentException("Invalid TimeUnit string: $value") + } + else -> throw WrappedIllegalArgumentException("Invalid TimeUnit argument: ${value.jsBrief()}") + } + // Java object: Proxy/Dispatcher/ConnectionPool/Authenticator/SSLSocketFactory/... + paramType.isInstance(value) -> value + // Let reflection verify the type matching by itself. + // zh-CN: 让反射自行校验类型是否匹配. + else -> value + } + + fun findCandidateMethods(name: String): List { + return builderClass.methods.filter { it.name == name && it.declaringClass == builderClass } + } + + clientProp.forEach { entry -> + val (rawKey, rawVal) = entry + val methodName = coerceString(rawKey, "").trim() + if (methodName.isEmpty()) return@forEach + + val candidates = findCandidateMethods(methodName) + require(candidates.isNotEmpty()) { + "No such Builder method: $methodName on ${builderClass.name}" + } + + val zeroArg = candidates.firstOrNull { it.parameterCount == 0 } + require(zeroArg == null) { + "Builder method \"$methodName\" with 0 parameter is not allowed to be invoked via client options" + } + val twoArgs = candidates.firstOrNull { it.parameterCount == 2 } + if (twoArgs != null && rawVal is List<*> && rawVal.size == 2) { + val args2 = arrayOf(coerceArg(twoArgs.parameterTypes[0], rawVal[0]), coerceArg(twoArgs.parameterTypes[1], rawVal[1])) + twoArgs.invoke(builder, args2[0], args2[1]) + return@forEach + } + + val oneArg = candidates.firstOrNull { it.parameterCount == 1 } + if (oneArg != null) { + val paramTypes = oneArg.parameterTypes + val arg0 = coerceArg(paramTypes[0], rawVal) + oneArg.invoke(builder, arg0) + return@forEach + } + + val supported = candidates.joinToString { "(${it.parameterTypes.joinToString { p -> p.simpleName }})" } + throw WrappedIllegalArgumentException( + "Builder method \"$methodName\" is not invokable with 1 or 2 parameters via client options. Supported overloads: $supported" + ) + } + + if (isInsecure) { + @SuppressLint("CustomX509TrustManager") + val trustAllCerts = arrayOf(object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) = Unit + override fun checkServerTrusted(chain: Array, authType: String) = Unit + override fun getAcceptedIssuers(): Array = arrayOf() + }) + val sslContext = SSLContext.getInstance("TLS").apply { + init(null, trustAllCerts, SecureRandom()) + } + val trustManager = trustAllCerts.first() as X509TrustManager + builder.sslSocketFactory(sslContext.socketFactory, trustManager) + builder.hostnameVerifier { _, _ -> true } + } + + // Apply the new Builder to the internal client. + // zh-CN: 应用新的 Builder 到内部客户端. + muteClient(builder) + } + private class ResponseWrapper( private val scriptRuntime: ScriptRuntime, private val res: Response, diff --git a/version.properties b/version.properties index 31a51004..db298d71 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Sun Oct 26 14:38:37 CST 2025 -BUILD_TIME=1761460717246 +#Sun Oct 26 15:58:01 CST 2025 +BUILD_TIME=1761465481857 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125 @@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=36 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3440 +VERSION_BUILD=3441 VERSION_NAME=6.7.0 Alpha8 VSCODE_EXT_REQUIRED_VERSION=1.0.8