6.7.0 - Alpha13 - http 模块请求相关方法修复 timeout/isInsecure 等选项功能, 支持更多 body 对象可用方法
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$data": {
|
"$data": {
|
||||||
"v6.7.0": {
|
"v6.7.0": {
|
||||||
"released_date": "2025/12/25",
|
"released_date": "2025/12/26",
|
||||||
"feature": [
|
"feature": [
|
||||||
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
|
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
|
||||||
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
||||||
@@ -9,12 +9,12 @@
|
|||||||
"zip 模块, 用于文件压缩与解压缩操作 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) (参阅 项目文档 > [Zip](https://docs.autojs6.com/#/zip))",
|
"zip 模块, 用于文件压缩与解压缩操作 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) (参阅 项目文档 > [Zip](https://docs.autojs6.com/#/zip))",
|
||||||
"mediainfo 模块, 用于查看媒体文件的详细信息 (参阅 项目文档 > [媒体信息](https://docs.autojs6.com/#/mediainfo))",
|
"mediainfo 模块, 用于查看媒体文件的详细信息 (参阅 项目文档 > [媒体信息](https://docs.autojs6.com/#/mediainfo))",
|
||||||
"cvt.bytes 方法, 用于字节数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
"cvt.bytes 方法, 用于字节数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
||||||
"fmt.bytes 方法, 用于字节数据格式化 (参阅 项目文档 > [数据格式化](https://docs.autojs6.com/#/fmt))",
|
"fmt.bytes 方法, 用于字节数据格式化 (参阅 项目文档 > [格式化](https://docs.autojs6.com/#/fmt))",
|
||||||
"s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))",
|
"s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))",
|
||||||
"app.isDualInstalled 方法, 用于检测双开应用是否已安装 (需要 Shizuku 或 Root 权限) _[`issue #450`](http://issues.autojs6.com/450)_",
|
"app.isDualInstalled 方法, 用于检测双开应用是否已安装 (需要 Shizuku 或 Root 权限) _[`issue #450`](http://issues.autojs6.com/450)_",
|
||||||
"device.getSharedDeviceId 方法, 用于跨应用获取统一共享设备 ID _[`issue #455`](http://issues.autojs6.com/455)_",
|
"device.getSharedDeviceId 方法, 用于跨应用获取统一共享设备 ID _[`issue #455`](http://issues.autojs6.com/455)_",
|
||||||
"ui.navigationBarHeight 属性 (getter), 用于获取导航栏高度 _[`issue #456`](http://issues.autojs6.com/456)_",
|
"ui.navigationBarHeight 属性 (getter), 用于获取导航栏高度 _[`issue #456`](http://issues.autojs6.com/456)_",
|
||||||
"http 模块请求相关方法获取的 body 对象增加 stream/saveToFile/close 方法 _[`issue #452`](http://issues.autojs6.com/452)_",
|
"http 模块请求相关方法获取的 body 对象增加 stream/saveToFile/close 等方法 _[`issue #452`](http://issues.autojs6.com/452)_",
|
||||||
"http 模块请求相关方法支持缓存控制选项参数 (cacheBody/bodyCacheThresholdBytes)",
|
"http 模块请求相关方法支持缓存控制选项参数 (cacheBody/bodyCacheThresholdBytes)",
|
||||||
"http 模块请求相关方法支持不安全选项参数 (isInsecure/insecure), 用于忽略证书相关异常 _[`issue #417`](http://issues.autojs6.com/417)_",
|
"http 模块请求相关方法支持不安全选项参数 (isInsecure/insecure), 用于忽略证书相关异常 _[`issue #417`](http://issues.autojs6.com/417)_",
|
||||||
"http 模块请求相关方法支持 options.client 选项, 用于配置 OkHttpClient.Builder (如 followRedirects 等) _[`issue #454`](http://issues.autojs6.com/454)_",
|
"http 模块请求相关方法支持 options.client 选项, 用于配置 OkHttpClient.Builder (如 followRedirects 等) _[`issue #454`](http://issues.autojs6.com/454)_",
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ class RootAutomatorNativeObject(scriptRuntime: ScriptRuntime, waitForReady: Any?
|
|||||||
defineProperty("__ra__", mRootAutomatorObject, READONLY or DONTENUM or PERMANENT)
|
defineProperty("__ra__", mRootAutomatorObject, READONLY or DONTENUM or PERMANENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun has(name: String?): Boolean {
|
override fun has(name: String, start: Scriptable): Boolean {
|
||||||
return mRootAutomatorObject.has(name) || super.has(name)
|
return mRootAutomatorObject.has(name, start) || super.has(name, start)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(name: String, start: Scriptable): Any? {
|
override fun get(name: String, start: Scriptable): Any? {
|
||||||
|
|||||||
@@ -2,82 +2,46 @@
|
|||||||
|
|
||||||
package org.autojs.autojs.runtime.api.augment.http
|
package org.autojs.autojs.runtime.api.augment.http
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.MediaType
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.MultipartBody
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import okio.BufferedSink
|
|
||||||
import org.autojs.autojs.annotation.RhinoFunctionBody
|
import org.autojs.autojs.annotation.RhinoFunctionBody
|
||||||
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
|
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
|
||||||
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
|
|
||||||
import org.autojs.autojs.core.http.MutableOkHttp
|
import org.autojs.autojs.core.http.MutableOkHttp
|
||||||
import org.autojs.autojs.extension.AnyExtensions.isJsFunction
|
import org.autojs.autojs.extension.AnyExtensions.isJsFunction
|
||||||
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
|
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
|
||||||
import org.autojs.autojs.extension.AnyExtensions.jsBrief
|
import org.autojs.autojs.extension.AnyExtensions.jsBrief
|
||||||
import org.autojs.autojs.extension.AnyExtensions.toRuntimePath
|
|
||||||
import org.autojs.autojs.extension.ArrayExtensions.toNativeArray
|
|
||||||
import org.autojs.autojs.extension.FlexibleArray
|
|
||||||
import org.autojs.autojs.extension.FlexibleArray.Companion.component1
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component1
|
||||||
import org.autojs.autojs.extension.FlexibleArray.Companion.component2
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component2
|
||||||
import org.autojs.autojs.extension.FlexibleArray.Companion.component3
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component3
|
||||||
import org.autojs.autojs.extension.FlexibleArray.Companion.component4
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component4
|
||||||
import org.autojs.autojs.extension.ScriptableExtensions.prop
|
import org.autojs.autojs.extension.ScriptableExtensions.prop
|
||||||
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
|
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
|
||||||
import org.autojs.autojs.pio.PFile
|
|
||||||
import org.autojs.autojs.pio.PFileInterface
|
|
||||||
import org.autojs.autojs.runtime.ScriptRuntime
|
import org.autojs.autojs.runtime.ScriptRuntime
|
||||||
import org.autojs.autojs.runtime.api.Mime
|
import org.autojs.autojs.runtime.api.Mime
|
||||||
import org.autojs.autojs.runtime.api.StringReadable
|
|
||||||
import org.autojs.autojs.runtime.api.augment.Augmentable
|
import org.autojs.autojs.runtime.api.augment.Augmentable
|
||||||
import org.autojs.autojs.runtime.api.augment.continuation.Continuation
|
import org.autojs.autojs.runtime.api.augment.continuation.Continuation
|
||||||
import org.autojs.autojs.runtime.api.augment.continuation.Creator
|
import org.autojs.autojs.runtime.api.augment.continuation.Creator
|
||||||
import org.autojs.autojs.runtime.api.augment.converter.core.Bytes
|
import org.autojs.autojs.runtime.api.augment.http.RequestBuilder.Companion.applyOkHttpClientBuilder
|
||||||
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
|
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
|
||||||
import org.autojs.autojs.util.RhinoUtils
|
|
||||||
import org.autojs.autojs.util.RhinoUtils.UNDEFINED
|
import org.autojs.autojs.util.RhinoUtils.UNDEFINED
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
|
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
|
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceLongNumber
|
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.coerceObject
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceString
|
import org.autojs.autojs.util.RhinoUtils.coerceString
|
||||||
import org.autojs.autojs.util.RhinoUtils.isUiThread
|
import org.autojs.autojs.util.RhinoUtils.isUiThread
|
||||||
import org.autojs.autojs.util.RhinoUtils.js_json_parse
|
|
||||||
import org.autojs.autojs.util.RhinoUtils.js_json_stringify
|
import org.autojs.autojs.util.RhinoUtils.js_json_stringify
|
||||||
import org.autojs.autojs.util.RhinoUtils.newNativeObject
|
import org.autojs.autojs.util.RhinoUtils.newNativeObject
|
||||||
import org.autojs.autojs.util.RhinoUtils.withRhinoContext
|
|
||||||
import org.mozilla.javascript.BaseFunction
|
import org.mozilla.javascript.BaseFunction
|
||||||
import org.mozilla.javascript.Context
|
import org.mozilla.javascript.Context
|
||||||
import org.mozilla.javascript.Function
|
|
||||||
import org.mozilla.javascript.NativeArray
|
|
||||||
import org.mozilla.javascript.NativeObject
|
import org.mozilla.javascript.NativeObject
|
||||||
import org.mozilla.javascript.Scriptable
|
|
||||||
import org.mozilla.javascript.ScriptableObject.DONTENUM
|
import org.mozilla.javascript.ScriptableObject.DONTENUM
|
||||||
import org.mozilla.javascript.ScriptableObject.PERMANENT
|
import org.mozilla.javascript.ScriptableObject.PERMANENT
|
||||||
import org.mozilla.javascript.ScriptableObject.READONLY
|
import org.mozilla.javascript.ScriptableObject.READONLY
|
||||||
import org.mozilla.javascript.Undefined
|
|
||||||
import java.io.IOException
|
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")
|
@Suppress("unused", "UNUSED_PARAMETER")
|
||||||
class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
|
class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
|
||||||
@@ -97,20 +61,21 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
|
|||||||
|
|
||||||
companion object : Augmentable() {
|
companion object : Augmentable() {
|
||||||
|
|
||||||
private const val METHOD_GET = "GET"
|
internal const val KEY_METHOD = "method"
|
||||||
private const val METHOD_POST = "POST"
|
internal const val KEY_CONTENT_TYPE = "contentType"
|
||||||
|
internal const val KEY_HEADERS = "headers"
|
||||||
|
internal const val KEY_FILES = "files"
|
||||||
|
internal const val KEY_BODY = "body"
|
||||||
|
internal const val KEY_CLIENT = "client"
|
||||||
|
internal const val KEY_TIMEOUT = "timeout"
|
||||||
|
|
||||||
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"
|
|
||||||
private const val KEY_FILES = "files"
|
|
||||||
private const val KEY_BODY = "body"
|
|
||||||
private const val KEY_TIMEOUT = "timeout"
|
|
||||||
private const val KEY_MAX_RETRIES = "maxRetries"
|
private const val KEY_MAX_RETRIES = "maxRetries"
|
||||||
private const val KEY_CACHE_BODY = "cacheBody"
|
private const val KEY_CACHE_BODY = "cacheBody"
|
||||||
private const val KEY_BODY_CACHE_THRESHOLD_BYTES = "bodyCacheThresholdBytes"
|
private const val KEY_BODY_CACHE_THRESHOLD_BYTES = "bodyCacheThresholdBytes"
|
||||||
|
|
||||||
|
private const val METHOD_GET = "GET"
|
||||||
|
private const val METHOD_POST = "POST"
|
||||||
|
|
||||||
private val DEFAULT_CONTENT_TYPE = Mime.APPLICATION_X_WWW_FORM_URLENCODED
|
private val DEFAULT_CONTENT_TYPE = Mime.APPLICATION_X_WWW_FORM_URLENCODED
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -164,7 +129,7 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
|
|||||||
applyOkHttpClientBuilder(opt)
|
applyOkHttpClientBuilder(opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cacheBody = opt.inquire(listOf(KEY_CACHE_BODY), ::coerceBoolean, DEFAULT_CACHE_BODY)
|
val cacheBody = opt.inquire(KEY_CACHE_BODY, ::coerceBoolean, DEFAULT_CACHE_BODY)
|
||||||
val cacheThreshold = coerceLongNumber(opt.prop(KEY_BODY_CACHE_THRESHOLD_BYTES), DEFAULT_BODY_CACHE_THRESHOLD_BYTES)
|
val cacheThreshold = coerceLongNumber(opt.prop(KEY_BODY_CACHE_THRESHOLD_BYTES), DEFAULT_BODY_CACHE_THRESHOLD_BYTES)
|
||||||
|
|
||||||
val newCall = scriptRuntime.http.client().newCall(buildRequestRhinoWithRuntime(scriptRuntime, url, options))
|
val newCall = scriptRuntime.http.client().newCall(buildRequestRhinoWithRuntime(scriptRuntime, url, options))
|
||||||
@@ -288,507 +253,6 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableOkHttp.applyOkHttpClientBuilder(opt: NativeObject) {
|
|
||||||
val clientProp = opt.prop(KEY_CLIENT).takeUnless { it.isJsNullish() }
|
|
||||||
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<Method> {
|
|
||||||
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<TrustManager>(object : X509TrustManager {
|
|
||||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
||||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
||||||
override fun getAcceptedIssuers(): Array<X509Certificate> = 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,
|
|
||||||
private val cacheBody: Boolean,
|
|
||||||
private val cacheThresholdBytes: Long,
|
|
||||||
) {
|
|
||||||
private val mRequest = res.request
|
|
||||||
|
|
||||||
var resBodyString: String? = null
|
|
||||||
var resBodyBytes: ByteArray? = null
|
|
||||||
|
|
||||||
fun wrap() = newNativeObject().apply {
|
|
||||||
put("request", this, mRequest)
|
|
||||||
put("statusMessage", this, res.message)
|
|
||||||
put("statusCode", this, res.code)
|
|
||||||
put("body", this, getBody())
|
|
||||||
put("headers", this, getHeaders())
|
|
||||||
put("url", this, mRequest.url)
|
|
||||||
put("method", this, mRequest.method)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBody(): ResponseBodyNativeObject {
|
|
||||||
|
|
||||||
// Returns a non-null value if this response
|
|
||||||
// was passed to Callback.onResponse
|
|
||||||
// or returned from Call.execute.
|
|
||||||
val resBody = res.body!!
|
|
||||||
|
|
||||||
return ResponseBodyNativeObject(scriptRuntime, resBody, this, cacheBody, cacheThresholdBytes).also {
|
|
||||||
it.defineFunctionProperties(arrayOf("string", "bytes", "json", "stream", "saveToFile", "close"), it.javaClass, READONLY or PERMANENT)
|
|
||||||
it.defineProperty(KEY_CONTENT_TYPE, { resBody.contentType() }, null, READONLY or PERMANENT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getHeaders(): NativeObject {
|
|
||||||
val result = newNativeObject()
|
|
||||||
val headers = res.headers
|
|
||||||
for (i in 0 until headers.size) {
|
|
||||||
val name = headers.name(i).lowercase()
|
|
||||||
val value = headers.value(i)
|
|
||||||
if (!result.containsKey(name)) {
|
|
||||||
result.put(name, result, value)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val list = mutableListOf<Any?>()
|
|
||||||
val origin = result.prop(name)
|
|
||||||
if (origin !is NativeArray) {
|
|
||||||
list += origin
|
|
||||||
} else {
|
|
||||||
list.addAll(origin)
|
|
||||||
}
|
|
||||||
list += value
|
|
||||||
result.put(name, result, list.toNativeArray())
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class HttpSaveResult @JvmOverloads constructor(
|
|
||||||
private val resultCode: Int,
|
|
||||||
private val outPath: String?,
|
|
||||||
private val bytesCopied: Long,
|
|
||||||
private val error: Throwable? = null,
|
|
||||||
) : NativeObject(), StringReadable {
|
|
||||||
|
|
||||||
init {
|
|
||||||
RhinoUtils.initNativeObjectPrototype(this)
|
|
||||||
defineProperty("code", { resultCode }, null, READONLY or PERMANENT)
|
|
||||||
defineProperty("path", { outPath }, null, READONLY or PERMANENT)
|
|
||||||
defineProperty("bytesCopied", { bytesCopied }, null, READONLY or PERMANENT)
|
|
||||||
defineProperty("success", { resultCode == RESULT_OK }, null, READONLY or PERMANENT)
|
|
||||||
defineProperty("error", { error }, null, READONLY or PERMANENT)
|
|
||||||
defineFunctionProperties(arrayOf("isSuccess"), javaClass, READONLY or PERMANENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toStringReadable(): String = listOf(
|
|
||||||
"${HttpSaveResult::class.java.simpleName} {",
|
|
||||||
" code: ${resultCode},",
|
|
||||||
" bytesCopied: $bytesCopied (${Bytes.string(bytesCopied.toDouble(), useSpace = true, strict = true)}),",
|
|
||||||
" path: '${outPath}',",
|
|
||||||
" error: ${error?.message?.take(256)?.let { "'$it'" }},",
|
|
||||||
"}",
|
|
||||||
).joinToString("\n")
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val RESULT_OK = 0
|
|
||||||
|
|
||||||
private const val RESULT_FAILED_GENERIC = -1
|
|
||||||
|
|
||||||
fun ok(path: String, bytesCopied: Long) =
|
|
||||||
HttpSaveResult(RESULT_OK, path, bytesCopied, null)
|
|
||||||
|
|
||||||
fun fail(path: String?, bytesCopied: Long, e: Throwable?) =
|
|
||||||
HttpSaveResult(RESULT_FAILED_GENERIC, path, bytesCopied, e)
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@RhinoStandardFunctionInterface
|
|
||||||
fun isSuccess(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Boolean = ensureArgumentsIsEmpty(args) {
|
|
||||||
val o = thisObj as HttpSaveResult
|
|
||||||
o.resultCode == RESULT_OK
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
private class ResponseBodyNativeObject(
|
|
||||||
val scriptRuntime: ScriptRuntime,
|
|
||||||
val resBody: ResponseBody,
|
|
||||||
val responseWrapper: ResponseWrapper,
|
|
||||||
private val cacheBody: Boolean,
|
|
||||||
private val cacheThresholdBytes: Long,
|
|
||||||
) : NativeObject() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
RhinoUtils.initNativeObjectPrototype(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record whether it has been explicitly closed.
|
|
||||||
// zh-CN: 记录是否已显式关闭.
|
|
||||||
@Volatile
|
|
||||||
private var closed = false
|
|
||||||
|
|
||||||
private fun ensureOpen() {
|
|
||||||
if (closed) throw IllegalStateException("Response body already closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun autoCloseIfNeeded() {
|
|
||||||
// Close immediately after reading the complete content to avoid resource leaks;
|
|
||||||
// stream() close is handled by the caller.
|
|
||||||
// zh-CN: 读取完整内容后立即关闭, 避免资源泄露; stream() 由调用者负责 close().
|
|
||||||
if (!closed) {
|
|
||||||
runCatching { resBody.close() }
|
|
||||||
closed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldCache(lengthHint: Long?): Boolean {
|
|
||||||
if (!cacheBody) return false
|
|
||||||
if (lengthHint == null || lengthHint < 0) return true
|
|
||||||
return lengthHint <= cacheThresholdBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object : FlexibleArray() {
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@RhinoStandardFunctionInterface
|
|
||||||
fun string(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): String = ensureArgumentsIsEmpty(args) {
|
|
||||||
val o = thisObj as ResponseBodyNativeObject
|
|
||||||
o.responseWrapper.resBodyString?.let { return@ensureArgumentsIsEmpty it }
|
|
||||||
o.ensureOpen()
|
|
||||||
val contentLength = runCatching {
|
|
||||||
o.resBody.contentLength()
|
|
||||||
}.getOrDefault(-1L)
|
|
||||||
val str = o.resBody.string()
|
|
||||||
if (o.shouldCache(contentLength)) {
|
|
||||||
o.responseWrapper.resBodyString = str
|
|
||||||
}
|
|
||||||
// string() can safely close after consuming the stream.
|
|
||||||
// zh-CN: string() 消费流后可安全关闭.
|
|
||||||
o.autoCloseIfNeeded()
|
|
||||||
return@ensureArgumentsIsEmpty str
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@RhinoStandardFunctionInterface
|
|
||||||
fun bytes(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): ByteArray = ensureArgumentsIsEmpty(args) {
|
|
||||||
val o = thisObj as ResponseBodyNativeObject
|
|
||||||
o.responseWrapper.resBodyBytes?.let { return@ensureArgumentsIsEmpty it }
|
|
||||||
o.ensureOpen()
|
|
||||||
val contentLength = runCatching {
|
|
||||||
o.resBody.contentLength()
|
|
||||||
}.getOrDefault(-1L)
|
|
||||||
val data = o.resBody.bytes()
|
|
||||||
if (o.shouldCache(contentLength)) {
|
|
||||||
o.responseWrapper.resBodyBytes = data
|
|
||||||
}
|
|
||||||
// bytes() 消费流后可安全关闭
|
|
||||||
o.autoCloseIfNeeded()
|
|
||||||
return@ensureArgumentsIsEmpty data
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@RhinoStandardFunctionInterface
|
|
||||||
fun json(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Any? = ensureArgumentsIsEmpty(args) {
|
|
||||||
val str = string(cx, thisObj, args, funObj)
|
|
||||||
runCatching {
|
|
||||||
return@ensureArgumentsIsEmpty js_json_parse(str)
|
|
||||||
}.onFailure {
|
|
||||||
throw IllegalStateException("Failed to parse JSON. Body string may be not in JSON format")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@RhinoStandardFunctionInterface
|
|
||||||
fun stream(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): InputStream = ensureArgumentsIsEmpty(args) {
|
|
||||||
val o = thisObj as ResponseBodyNativeObject
|
|
||||||
o.ensureOpen()
|
|
||||||
// Don't auto-close; let the caller handle it, supporting streaming copy.
|
|
||||||
// zh-CN: 不自动关闭; 交给调用者处理, 支持流式拷贝.
|
|
||||||
o.resBody.byteStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save directly to file (avoid loading large responses into memory).
|
|
||||||
// zh-CN: 直接保存到文件 (避免将大型响应加载到内存中).
|
|
||||||
@JvmStatic
|
|
||||||
@RhinoStandardFunctionInterface
|
|
||||||
fun saveToFile(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): HttpSaveResult = ensureArgumentsLengthInRange(args, 1..2) { argList ->
|
|
||||||
val o = thisObj as ResponseBodyNativeObject
|
|
||||||
o.ensureOpen()
|
|
||||||
|
|
||||||
val (pathRaw, bufSizeRaw) = argList
|
|
||||||
val path = coerceString(pathRaw, "").toRuntimePath(o.scriptRuntime)
|
|
||||||
val bufSize = coerceIntNumber(bufSizeRaw, 0).let {
|
|
||||||
if (it > 0) it else 8192
|
|
||||||
}
|
|
||||||
|
|
||||||
val file = PFile(path)
|
|
||||||
val isDirectory = file.isDirectory || path.endsWith("/")
|
|
||||||
if (isDirectory) {
|
|
||||||
throw WrappedIllegalArgumentException("Path \"$path\" must be a file path instead of a directory path")
|
|
||||||
}
|
|
||||||
val buffer = ByteArray(bufSize)
|
|
||||||
var copied = 0L
|
|
||||||
var input: InputStream? = null
|
|
||||||
var output: OutputStream? = null
|
|
||||||
return@ensureArgumentsLengthInRange try {
|
|
||||||
input = o.resBody.byteStream()
|
|
||||||
output = file.outputStream()
|
|
||||||
while (true) {
|
|
||||||
val read = input.read(buffer)
|
|
||||||
if (read == -1) break
|
|
||||||
output.write(buffer, 0, read)
|
|
||||||
copied += read
|
|
||||||
}
|
|
||||||
output.flush()
|
|
||||||
HttpSaveResult.ok(path, copied)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
HttpSaveResult.fail(path, copied, e)
|
|
||||||
} finally {
|
|
||||||
runCatching { input?.close() }
|
|
||||||
runCatching { output?.close() }
|
|
||||||
o.autoCloseIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit close.
|
|
||||||
// zh-CN: 显式关闭.
|
|
||||||
@JvmStatic
|
|
||||||
@RhinoStandardFunctionInterface
|
|
||||||
fun close(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Undefined = ensureArgumentsIsEmpty(args) {
|
|
||||||
val o = thisObj as ResponseBodyNativeObject
|
|
||||||
if (!o.closed) {
|
|
||||||
runCatching { o.resBody.close() }
|
|
||||||
o.closed = true
|
|
||||||
}
|
|
||||||
UNDEFINED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RequestBuilder(private val scriptRuntime: ScriptRuntime, private val url: Any?, options: NativeObject = newNativeObject()) {
|
|
||||||
|
|
||||||
private val mRequest = Request.Builder()
|
|
||||||
private val mRequestBuilderHelper = RequestBuilderHelper(options)
|
|
||||||
|
|
||||||
fun build(): Request {
|
|
||||||
mRequest.url(mRequestBuilderHelper.getUrl(coerceString(url)))
|
|
||||||
mRequestBuilderHelper.setHeaders(mRequest)
|
|
||||||
mRequestBuilderHelper.setMethod(scriptRuntime, mRequest)
|
|
||||||
return mRequest.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RequestBuilderHelper(private val options: NativeObject) {
|
|
||||||
|
|
||||||
@Suppress("HttpUrlsUsage")
|
|
||||||
fun getUrl(url: String) = when {
|
|
||||||
url.matches(Regex("^https?://.*")) -> url
|
|
||||||
else -> "http://$url"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHeaders(request: Request.Builder) {
|
|
||||||
val headers = options.prop(KEY_HEADERS)
|
|
||||||
if (headers.isJsNullish()) return
|
|
||||||
require(headers is NativeObject) { "Property headers ${headers.jsBrief()} for builder of http.request must be a JavaScript Object" }
|
|
||||||
headers.forEach { entry ->
|
|
||||||
val (key, value) = entry
|
|
||||||
when (value) {
|
|
||||||
is NativeArray -> value.forEach { setHeader(request, key, it) }
|
|
||||||
else -> setHeader(request, key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMethod(scriptRuntime: ScriptRuntime, request: Request.Builder) {
|
|
||||||
val method = coerceString(options.prop(KEY_METHOD))
|
|
||||||
// require(method is String) { "Property method is required for header options" }
|
|
||||||
when {
|
|
||||||
!options.prop(KEY_BODY).isJsNullish() -> {
|
|
||||||
request.method(method, parseBody())
|
|
||||||
}
|
|
||||||
!options.prop(KEY_FILES).isJsNullish() -> {
|
|
||||||
request.method(method, parseMultipart(scriptRuntime))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
request.method(method, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseBody(): RequestBody = when (val body = options.prop(KEY_BODY)) {
|
|
||||||
is RequestBody -> body
|
|
||||||
is String -> {
|
|
||||||
val mediaType = options.prop(KEY_CONTENT_TYPE).takeUnless { it.isJsNullish() }
|
|
||||||
body.toRequestBody(Context.toString(mediaType).toMediaTypeOrNull())
|
|
||||||
}
|
|
||||||
is BaseFunction -> object : RequestBody() {
|
|
||||||
override fun contentType(): MediaType? {
|
|
||||||
val mediaType = options.prop(KEY_CONTENT_TYPE).takeUnless { it.isJsNullish() }
|
|
||||||
return Context.toString(mediaType).toMediaTypeOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeTo(sink: BufferedSink) {
|
|
||||||
withRhinoContext { cx ->
|
|
||||||
body.call(cx, body, body, arrayOf(sink))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw WrappedIllegalArgumentException("Unknown type of body for header options")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseMultipart(scriptRuntime: ScriptRuntime): MultipartBody {
|
|
||||||
val builder = MultipartBody.Builder().setType(MultipartBody.FORM)
|
|
||||||
|
|
||||||
val files = options.prop(KEY_FILES)
|
|
||||||
if (files.isJsNullish()) return builder.build()
|
|
||||||
require(files is NativeObject) { "Property files ${files.jsBrief()} for builder of http.request must be a JavaScript Object" }
|
|
||||||
|
|
||||||
files.forEach { entry ->
|
|
||||||
val (key, value) = entry
|
|
||||||
when (value) {
|
|
||||||
is String -> {
|
|
||||||
builder.addFormDataPart(coerceString(key), value)
|
|
||||||
}
|
|
||||||
is NativeArray -> when (value.length) {
|
|
||||||
2L -> {
|
|
||||||
val (fileName, path) = value
|
|
||||||
val file = if (path is URI) PFile(path) else PFile(path.toRuntimePath(scriptRuntime))
|
|
||||||
val mimeType = parseMimeType(file.extension)
|
|
||||||
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
|
|
||||||
processFile(builder, key, fileName, requestBody)
|
|
||||||
}
|
|
||||||
3L -> {
|
|
||||||
val (fileName, mimeType, path) = value
|
|
||||||
val file = if (path is URI) PFile(path) else PFile(path.toRuntimePath(scriptRuntime))
|
|
||||||
val requestBody = file.asRequestBody(coerceString(mimeType).toMediaTypeOrNull())
|
|
||||||
processFile(builder, key, fileName, requestBody)
|
|
||||||
}
|
|
||||||
else -> listOf(
|
|
||||||
"Array value \"value\" for property \"files\"",
|
|
||||||
"in RequestBuilderHelper#parseMultipart",
|
|
||||||
"must be of length 2 or 3 instead of ${value.length}",
|
|
||||||
).joinToString(" ").let { throw WrappedIllegalArgumentException(it) }
|
|
||||||
}
|
|
||||||
is PFileInterface -> {
|
|
||||||
val path = value.path
|
|
||||||
val file = PFile(path.toRuntimePath(scriptRuntime))
|
|
||||||
val fileName = file.name
|
|
||||||
val mimeType = parseMimeType(file.extension)
|
|
||||||
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
|
|
||||||
processFile(builder, key, fileName, requestBody)
|
|
||||||
}
|
|
||||||
else -> listOf(
|
|
||||||
"Value \"value\" for property \"files\"",
|
|
||||||
"in RequestBuilderHelper#parseMultipart",
|
|
||||||
"must be either a string, an array",
|
|
||||||
"or a JavaScript object",
|
|
||||||
).joinToString(" ").let { throw WrappedIllegalArgumentException(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseMimeType(ext: String) = when {
|
|
||||||
ext.isNotEmpty() -> {
|
|
||||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: Mime.APPLICATION_OCTET_STREAM
|
|
||||||
}
|
|
||||||
else -> Mime.APPLICATION_OCTET_STREAM
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setHeader(request: Request.Builder, key: Any?, it: Any?) {
|
|
||||||
request.header(coerceString(key), Context.toString(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processFile(builder: MultipartBody.Builder, key: Any?, fileName: Any?, requestBody: RequestBody) {
|
|
||||||
builder.addFormDataPart(coerceString(key), fileName as? String, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.autojs.autojs.runtime.api.augment.http
|
||||||
|
|
||||||
|
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
|
||||||
|
import org.autojs.autojs.extension.FlexibleArray.Companion.ensureArgumentsIsEmpty
|
||||||
|
import org.autojs.autojs.runtime.api.StringReadable
|
||||||
|
import org.autojs.autojs.runtime.api.augment.converter.core.Bytes
|
||||||
|
import org.autojs.autojs.util.RhinoUtils
|
||||||
|
import org.mozilla.javascript.Context
|
||||||
|
import org.mozilla.javascript.Function
|
||||||
|
import org.mozilla.javascript.NativeObject
|
||||||
|
import org.mozilla.javascript.Scriptable
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class HttpSaveResult @JvmOverloads constructor(
|
||||||
|
private val resultCode: Int,
|
||||||
|
private val outPath: String?,
|
||||||
|
private val bytesCopied: Long,
|
||||||
|
private val error: Throwable? = null,
|
||||||
|
) : NativeObject(), StringReadable {
|
||||||
|
|
||||||
|
init {
|
||||||
|
RhinoUtils.initNativeObjectPrototype(this)
|
||||||
|
defineProperty("code", { resultCode }, null, READONLY or PERMANENT)
|
||||||
|
defineProperty("path", { outPath }, null, READONLY or PERMANENT)
|
||||||
|
defineProperty("bytesCopied", { bytesCopied }, null, READONLY or PERMANENT)
|
||||||
|
defineProperty("success", { resultCode == RESULT_OK }, null, READONLY or PERMANENT)
|
||||||
|
defineProperty("error", { error }, null, READONLY or PERMANENT)
|
||||||
|
defineFunctionProperties(arrayOf("isSuccess"), javaClass, READONLY or PERMANENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toStringReadable(): String = listOf(
|
||||||
|
"${HttpSaveResult::class.java.simpleName} {",
|
||||||
|
" code: ${resultCode},",
|
||||||
|
" bytesCopied: $bytesCopied (${Bytes.string(bytesCopied.toDouble(), useSpace = true, strict = true)}),",
|
||||||
|
" path: '${outPath}',",
|
||||||
|
" error: ${error?.message?.take(256)?.let { "'$it'" }},",
|
||||||
|
"}",
|
||||||
|
).joinToString("\n")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val RESULT_OK = 0
|
||||||
|
|
||||||
|
private const val RESULT_FAILED_GENERIC = -1
|
||||||
|
|
||||||
|
fun ok(path: String, bytesCopied: Long) =
|
||||||
|
HttpSaveResult(RESULT_OK, path, bytesCopied, null)
|
||||||
|
|
||||||
|
fun fail(path: String?, bytesCopied: Long, e: Throwable?) =
|
||||||
|
HttpSaveResult(RESULT_FAILED_GENERIC, path, bytesCopied, e)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoStandardFunctionInterface
|
||||||
|
fun isSuccess(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Boolean = ensureArgumentsIsEmpty(args) {
|
||||||
|
val o = thisObj as HttpSaveResult
|
||||||
|
o.resultCode == RESULT_OK
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package org.autojs.autojs.runtime.api.augment.http
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.autojs.autojs.core.http.MutableOkHttp
|
||||||
|
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
|
||||||
|
import org.autojs.autojs.extension.AnyExtensions.jsBrief
|
||||||
|
import org.autojs.autojs.extension.ScriptableExtensions.prop
|
||||||
|
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
|
||||||
|
import org.autojs.autojs.runtime.ScriptRuntime
|
||||||
|
import org.autojs.autojs.runtime.api.augment.http.Http.Companion.DEFAULT_TIMEOUT
|
||||||
|
import org.autojs.autojs.runtime.api.augment.http.Http.Companion.KEY_CLIENT
|
||||||
|
import org.autojs.autojs.runtime.api.augment.http.Http.Companion.KEY_TIMEOUT
|
||||||
|
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
|
||||||
|
import org.autojs.autojs.util.RhinoUtils
|
||||||
|
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.coerceString
|
||||||
|
import org.mozilla.javascript.NativeObject
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
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
|
||||||
|
|
||||||
|
class RequestBuilder(
|
||||||
|
private val scriptRuntime: ScriptRuntime,
|
||||||
|
private val url: Any?,
|
||||||
|
options: NativeObject = RhinoUtils.newNativeObject(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val mRequest = Request.Builder()
|
||||||
|
private val mRequestBuilderHelper = RequestBuilderHelper(options)
|
||||||
|
|
||||||
|
fun build(): Request {
|
||||||
|
mRequest.url(mRequestBuilderHelper.getUrl(coerceString(url)))
|
||||||
|
mRequestBuilderHelper.setHeaders(mRequest)
|
||||||
|
mRequestBuilderHelper.setMethod(scriptRuntime, mRequest)
|
||||||
|
return mRequest.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun MutableOkHttp.applyOkHttpClientBuilder(opt: NativeObject) {
|
||||||
|
val clientProp = opt.prop(KEY_CLIENT).takeUnless { it.isJsNullish() }
|
||||||
|
require(clientProp is NativeObject?) { "Argument \"client\" ${clientProp.jsBrief()} for http.request must be a JavaScript Object" }
|
||||||
|
|
||||||
|
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 == Boolean::class.java -> coerceBoolean(value, false)
|
||||||
|
paramType == java.lang.Long.TYPE || paramType == Long::class.java -> coerceLongNumber(value, 0L)
|
||||||
|
paramType == Integer.TYPE || paramType == Int::class.java -> coerceIntNumber(value, 0)
|
||||||
|
paramType == java.lang.Double.TYPE || paramType == 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<Method> {
|
||||||
|
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<TrustManager>(object : X509TrustManager {
|
||||||
|
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
||||||
|
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate> = 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package org.autojs.autojs.runtime.api.augment.http
|
||||||
|
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okio.BufferedSink
|
||||||
|
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
|
||||||
|
import org.autojs.autojs.extension.AnyExtensions.isJsNumber
|
||||||
|
import org.autojs.autojs.extension.AnyExtensions.isJsString
|
||||||
|
import org.autojs.autojs.extension.AnyExtensions.jsBrief
|
||||||
|
import org.autojs.autojs.extension.AnyExtensions.toRuntimePath
|
||||||
|
import org.autojs.autojs.extension.ScriptableExtensions.prop
|
||||||
|
import org.autojs.autojs.pio.PFile
|
||||||
|
import org.autojs.autojs.pio.PFileInterface
|
||||||
|
import org.autojs.autojs.runtime.ScriptRuntime
|
||||||
|
import org.autojs.autojs.runtime.api.Mime
|
||||||
|
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
|
||||||
|
import org.autojs.autojs.util.RhinoUtils
|
||||||
|
import org.mozilla.javascript.BaseFunction
|
||||||
|
import org.mozilla.javascript.Context
|
||||||
|
import org.mozilla.javascript.NativeArray
|
||||||
|
import org.mozilla.javascript.NativeObject
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
class RequestBuilderHelper(private val options: NativeObject) {
|
||||||
|
|
||||||
|
@Suppress("HttpUrlsUsage")
|
||||||
|
fun getUrl(url: String) = when {
|
||||||
|
url.matches(Regex("^https?://.*")) -> url
|
||||||
|
else -> "http://$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHeaders(request: Request.Builder) {
|
||||||
|
val headers = options.prop(Http.KEY_HEADERS)
|
||||||
|
if (headers.isJsNullish()) return
|
||||||
|
require(headers is NativeObject) { "Property headers ${headers.jsBrief()} for builder of http.request must be a JavaScript Object" }
|
||||||
|
headers.forEach { entry ->
|
||||||
|
val (key, value) = entry
|
||||||
|
when (value) {
|
||||||
|
is NativeArray -> value.forEach { setHeader(request, key, it) }
|
||||||
|
else -> setHeader(request, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMethod(scriptRuntime: ScriptRuntime, request: Request.Builder) {
|
||||||
|
val method = RhinoUtils.coerceString(options.prop(Http.KEY_METHOD))
|
||||||
|
// require(method is String) { "Property method is required for header options" }
|
||||||
|
when {
|
||||||
|
!options.prop(Http.KEY_BODY).isJsNullish() -> {
|
||||||
|
request.method(method, parseBody())
|
||||||
|
}
|
||||||
|
!options.prop(Http.KEY_FILES).isJsNullish() -> {
|
||||||
|
request.method(method, parseMultipart(scriptRuntime))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
request.method(method, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseBody(): RequestBody = when (val body = options.prop(Http.KEY_BODY)) {
|
||||||
|
is RequestBody -> body
|
||||||
|
is String -> {
|
||||||
|
val mediaType = options.prop(Http.KEY_CONTENT_TYPE).takeUnless { it.isJsNullish() }
|
||||||
|
body.toRequestBody(Context.toString(mediaType).toMediaTypeOrNull())
|
||||||
|
}
|
||||||
|
is BaseFunction -> object : RequestBody() {
|
||||||
|
override fun contentType(): MediaType? {
|
||||||
|
val mediaType = options.prop(Http.KEY_CONTENT_TYPE).takeUnless { it.isJsNullish() }
|
||||||
|
return Context.toString(mediaType).toMediaTypeOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeTo(sink: BufferedSink) {
|
||||||
|
RhinoUtils.withRhinoContext { cx ->
|
||||||
|
body.call(cx, body, body, arrayOf(sink))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw WrappedIllegalArgumentException("Unknown type of body for header options")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseMultipart(scriptRuntime: ScriptRuntime): MultipartBody {
|
||||||
|
val builder = MultipartBody.Builder().setType(MultipartBody.FORM)
|
||||||
|
|
||||||
|
val files = options.prop(Http.KEY_FILES)
|
||||||
|
if (files.isJsNullish()) return builder.build()
|
||||||
|
require(files is NativeObject) { "Property files ${files.jsBrief()} for builder of http.request must be a JavaScript Object" }
|
||||||
|
|
||||||
|
files.forEach { entry ->
|
||||||
|
val (key, value) = entry
|
||||||
|
when {
|
||||||
|
value.isJsString() || value.isJsNumber() -> {
|
||||||
|
builder.addFormDataPart(RhinoUtils.coerceString(key), RhinoUtils.coerceString(value))
|
||||||
|
}
|
||||||
|
value is NativeArray -> when (value.length) {
|
||||||
|
2L -> {
|
||||||
|
val (fileName, path) = value
|
||||||
|
val file = if (path is URI) PFile(path) else PFile(path.toRuntimePath(scriptRuntime))
|
||||||
|
val mimeType = parseMimeType(file.extension)
|
||||||
|
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
|
||||||
|
processFile(builder, key, fileName, requestBody)
|
||||||
|
}
|
||||||
|
3L -> {
|
||||||
|
val (fileName, mimeType, path) = value
|
||||||
|
val file = if (path is URI) PFile(path) else PFile(path.toRuntimePath(scriptRuntime))
|
||||||
|
val requestBody = file.asRequestBody(RhinoUtils.coerceString(mimeType).toMediaTypeOrNull())
|
||||||
|
processFile(builder, key, fileName, requestBody)
|
||||||
|
}
|
||||||
|
else -> listOf(
|
||||||
|
"Array value \"value\" for property \"files\"",
|
||||||
|
"in RequestBuilderHelper#parseMultipart",
|
||||||
|
"must be of length 2 or 3 instead of ${value.length}",
|
||||||
|
).joinToString(" ").let { throw WrappedIllegalArgumentException(it) }
|
||||||
|
}
|
||||||
|
value is PFileInterface -> {
|
||||||
|
val path = value.path
|
||||||
|
val file = PFile(path.toRuntimePath(scriptRuntime))
|
||||||
|
val fileName = file.name
|
||||||
|
val mimeType = parseMimeType(file.extension)
|
||||||
|
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
|
||||||
|
processFile(builder, key, fileName, requestBody)
|
||||||
|
}
|
||||||
|
else -> listOf(
|
||||||
|
"Value \"value\" ${value.jsBrief()} for property \"files\"",
|
||||||
|
"in RequestBuilderHelper#parseMultipart",
|
||||||
|
"must be either a string, an array",
|
||||||
|
"or a JavaScript object",
|
||||||
|
).joinToString(" ").let { throw WrappedIllegalArgumentException(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseMimeType(ext: String) = when {
|
||||||
|
ext.isNotEmpty() -> {
|
||||||
|
MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: Mime.APPLICATION_OCTET_STREAM
|
||||||
|
}
|
||||||
|
else -> Mime.APPLICATION_OCTET_STREAM
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setHeader(request: Request.Builder, key: Any?, it: Any?) {
|
||||||
|
request.header(RhinoUtils.coerceString(key), Context.toString(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processFile(builder: MultipartBody.Builder, key: Any?, fileName: Any?, requestBody: RequestBody) {
|
||||||
|
builder.addFormDataPart(RhinoUtils.coerceString(key), fileName as? String, requestBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package org.autojs.autojs.runtime.api.augment.http
|
||||||
|
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
|
||||||
|
import org.autojs.autojs.extension.AnyExtensions.toRuntimePath
|
||||||
|
import org.autojs.autojs.extension.FlexibleArray
|
||||||
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component1
|
||||||
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component2
|
||||||
|
import org.autojs.autojs.extension.ScriptableExtensions.prop
|
||||||
|
import org.autojs.autojs.pio.PFile
|
||||||
|
import org.autojs.autojs.runtime.ScriptRuntime
|
||||||
|
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
|
||||||
|
import org.autojs.autojs.util.RhinoUtils
|
||||||
|
import org.autojs.autojs.util.RhinoUtils.getRhinoStandardFunctionMethods
|
||||||
|
import org.autojs.autojs.util.RhinoUtils.withRhinoContext
|
||||||
|
import org.mozilla.javascript.BaseFunction
|
||||||
|
import org.mozilla.javascript.BoundFunction
|
||||||
|
import org.mozilla.javascript.Context
|
||||||
|
import org.mozilla.javascript.Function
|
||||||
|
import org.mozilla.javascript.NativeObject
|
||||||
|
import org.mozilla.javascript.Scriptable
|
||||||
|
import org.mozilla.javascript.Undefined
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import org.mozilla.javascript.ScriptRuntime as RhinoScriptRuntime
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class ResponseBodyNativeObject(
|
||||||
|
val scriptRuntime: ScriptRuntime,
|
||||||
|
val resBody: ResponseBody,
|
||||||
|
val responseWrapper: ResponseWrapper,
|
||||||
|
private val cacheBody: Boolean,
|
||||||
|
private val cacheThresholdBytes: Long,
|
||||||
|
) : NativeObject() {
|
||||||
|
|
||||||
|
private val mResBodyObject: Scriptable by lazy {
|
||||||
|
RhinoScriptRuntime.toObject(scriptRuntime.topLevelScope, resBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
RhinoUtils.initNativeObjectPrototype(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record whether it has been explicitly closed.
|
||||||
|
// zh-CN: 记录是否已显式关闭.
|
||||||
|
@Volatile
|
||||||
|
var closed = false
|
||||||
|
|
||||||
|
override fun has(name: String, start: Scriptable): Boolean {
|
||||||
|
return mResBodyObject.has(name, start) || super.has(name, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(name: String, start: Scriptable): Any? {
|
||||||
|
val rhinoMethods = Companion::class.java.getRhinoStandardFunctionMethods()
|
||||||
|
rhinoMethods.firstOrNull { it.name == name }?.let {
|
||||||
|
return super.get(name, start)
|
||||||
|
}
|
||||||
|
return when (val o = mResBodyObject.prop(name)) {
|
||||||
|
is BaseFunction -> withRhinoContext { cx ->
|
||||||
|
BoundFunction(cx, mResBodyObject, o, mResBodyObject, arrayOf())
|
||||||
|
}
|
||||||
|
else -> super.get(name, start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureOpen() {
|
||||||
|
if (closed) throw IllegalStateException("Response body already closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun autoCloseIfNeeded() {
|
||||||
|
// Close immediately after reading the complete content to avoid resource leaks;
|
||||||
|
// stream() close is handled by the caller.
|
||||||
|
// zh-CN: 读取完整内容后立即关闭, 避免资源泄露; stream() 由调用者负责 close().
|
||||||
|
if (!closed) {
|
||||||
|
runCatching { resBody.close() }
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldCache(lengthHint: Long?): Boolean {
|
||||||
|
if (!cacheBody) return false
|
||||||
|
if (lengthHint == null || lengthHint < 0) return true
|
||||||
|
return lengthHint <= cacheThresholdBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : FlexibleArray() {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoStandardFunctionInterface
|
||||||
|
fun string(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): String = ensureArgumentsIsEmpty(args) {
|
||||||
|
val o = thisObj as ResponseBodyNativeObject
|
||||||
|
o.responseWrapper.resBodyString?.let { return@ensureArgumentsIsEmpty it }
|
||||||
|
o.ensureOpen()
|
||||||
|
val contentLength = runCatching {
|
||||||
|
o.resBody.contentLength()
|
||||||
|
}.getOrDefault(-1L)
|
||||||
|
val str = o.resBody.string()
|
||||||
|
if (o.shouldCache(contentLength)) {
|
||||||
|
o.responseWrapper.resBodyString = str
|
||||||
|
}
|
||||||
|
// string() can safely close after consuming the stream.
|
||||||
|
// zh-CN: string() 消费流后可安全关闭.
|
||||||
|
o.autoCloseIfNeeded()
|
||||||
|
return@ensureArgumentsIsEmpty str
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoStandardFunctionInterface
|
||||||
|
fun bytes(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): ByteArray = ensureArgumentsIsEmpty(args) {
|
||||||
|
val o = thisObj as ResponseBodyNativeObject
|
||||||
|
o.responseWrapper.resBodyBytes?.let { return@ensureArgumentsIsEmpty it }
|
||||||
|
o.ensureOpen()
|
||||||
|
val contentLength = runCatching {
|
||||||
|
o.resBody.contentLength()
|
||||||
|
}.getOrDefault(-1L)
|
||||||
|
val data = o.resBody.bytes()
|
||||||
|
if (o.shouldCache(contentLength)) {
|
||||||
|
o.responseWrapper.resBodyBytes = data
|
||||||
|
}
|
||||||
|
// bytes() 消费流后可安全关闭
|
||||||
|
o.autoCloseIfNeeded()
|
||||||
|
return@ensureArgumentsIsEmpty data
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoStandardFunctionInterface
|
||||||
|
fun json(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Any? = ensureArgumentsIsEmpty(args) {
|
||||||
|
val str = string(cx, thisObj, args, funObj)
|
||||||
|
runCatching {
|
||||||
|
return@ensureArgumentsIsEmpty RhinoUtils.js_json_parse(str)
|
||||||
|
}.onFailure {
|
||||||
|
throw IllegalStateException("Failed to parse JSON. Body string may be not in JSON format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoStandardFunctionInterface
|
||||||
|
fun stream(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): InputStream = ensureArgumentsIsEmpty(args) {
|
||||||
|
val o = thisObj as ResponseBodyNativeObject
|
||||||
|
o.ensureOpen()
|
||||||
|
// Don't auto-close; let the caller handle it, supporting streaming copy.
|
||||||
|
// zh-CN: 不自动关闭; 交给调用者处理, 支持流式拷贝.
|
||||||
|
o.resBody.byteStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save directly to file (avoid loading large responses into memory).
|
||||||
|
// zh-CN: 直接保存到文件 (避免将大型响应加载到内存中).
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoStandardFunctionInterface
|
||||||
|
fun saveToFile(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): HttpSaveResult = ensureArgumentsLengthInRange(args, 1..2) { argList ->
|
||||||
|
val o = thisObj as ResponseBodyNativeObject
|
||||||
|
o.ensureOpen()
|
||||||
|
|
||||||
|
val (pathRaw, bufSizeRaw) = argList
|
||||||
|
val path = RhinoUtils.coerceString(pathRaw, "").toRuntimePath(o.scriptRuntime)
|
||||||
|
val bufSize = RhinoUtils.coerceIntNumber(bufSizeRaw, 0).let {
|
||||||
|
if (it > 0) it else 8192
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = PFile(path)
|
||||||
|
val isDirectory = file.isDirectory || path.endsWith("/")
|
||||||
|
if (isDirectory) {
|
||||||
|
throw WrappedIllegalArgumentException("Path \"$path\" must be a file path instead of a directory path")
|
||||||
|
}
|
||||||
|
val buffer = ByteArray(bufSize)
|
||||||
|
var copied = 0L
|
||||||
|
var input: InputStream? = null
|
||||||
|
var output: OutputStream? = null
|
||||||
|
return@ensureArgumentsLengthInRange try {
|
||||||
|
input = o.resBody.byteStream()
|
||||||
|
output = file.outputStream()
|
||||||
|
while (true) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read == -1) break
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
copied += read
|
||||||
|
}
|
||||||
|
output.flush()
|
||||||
|
HttpSaveResult.ok(path, copied)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
HttpSaveResult.fail(path, copied, e)
|
||||||
|
} finally {
|
||||||
|
runCatching { input?.close() }
|
||||||
|
runCatching { output?.close() }
|
||||||
|
o.autoCloseIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit close.
|
||||||
|
// zh-CN: 显式关闭.
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoStandardFunctionInterface
|
||||||
|
fun close(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Undefined = ensureArgumentsIsEmpty(args) {
|
||||||
|
val o = thisObj as ResponseBodyNativeObject
|
||||||
|
if (!o.closed) {
|
||||||
|
runCatching { o.resBody.close() }
|
||||||
|
o.closed = true
|
||||||
|
}
|
||||||
|
RhinoUtils.UNDEFINED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package org.autojs.autojs.runtime.api.augment.http
|
||||||
|
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.autojs.autojs.extension.ArrayExtensions.toNativeArray
|
||||||
|
import org.autojs.autojs.extension.ScriptableExtensions.prop
|
||||||
|
import org.autojs.autojs.runtime.ScriptRuntime
|
||||||
|
import org.autojs.autojs.util.RhinoUtils.newNativeObject
|
||||||
|
import org.mozilla.javascript.NativeArray
|
||||||
|
import org.mozilla.javascript.NativeObject
|
||||||
|
import org.mozilla.javascript.ScriptableObject.PERMANENT
|
||||||
|
import org.mozilla.javascript.ScriptableObject.READONLY
|
||||||
|
|
||||||
|
class ResponseWrapper(
|
||||||
|
private val scriptRuntime: ScriptRuntime,
|
||||||
|
private val res: Response,
|
||||||
|
private val cacheBody: Boolean,
|
||||||
|
private val cacheThresholdBytes: Long,
|
||||||
|
) {
|
||||||
|
private val mRequest = res.request
|
||||||
|
|
||||||
|
var resBodyString: String? = null
|
||||||
|
var resBodyBytes: ByteArray? = null
|
||||||
|
|
||||||
|
fun wrap() = newNativeObject().apply {
|
||||||
|
put("request", this, mRequest)
|
||||||
|
put("statusMessage", this, res.message)
|
||||||
|
put("statusCode", this, res.code)
|
||||||
|
put("body", this, getBody())
|
||||||
|
put("headers", this, getHeaders())
|
||||||
|
put("url", this, mRequest.url)
|
||||||
|
put("method", this, mRequest.method)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBody(): ResponseBodyNativeObject {
|
||||||
|
|
||||||
|
// Returns a non-null value if this response
|
||||||
|
// was passed to Callback.onResponse
|
||||||
|
// or returned from Call.execute.
|
||||||
|
val resBody = res.body!!
|
||||||
|
|
||||||
|
return ResponseBodyNativeObject(scriptRuntime, resBody, this, cacheBody, cacheThresholdBytes).also {
|
||||||
|
it.defineFunctionProperties(arrayOf("string", "bytes", "json", "stream", "saveToFile", "close"), it.javaClass, READONLY or PERMANENT)
|
||||||
|
it.defineProperty(Http.KEY_CONTENT_TYPE, { resBody.contentType() }, null, READONLY or PERMANENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHeaders(): NativeObject {
|
||||||
|
val result = newNativeObject()
|
||||||
|
val headers = res.headers
|
||||||
|
for (i in 0 until headers.size) {
|
||||||
|
val name = headers.name(i).lowercase()
|
||||||
|
val value = headers.value(i)
|
||||||
|
if (!result.containsKey(name)) {
|
||||||
|
result.put(name, result, value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val list = mutableListOf<Any?>()
|
||||||
|
val origin = result.prop(name)
|
||||||
|
if (origin !is NativeArray) {
|
||||||
|
list += origin
|
||||||
|
} else {
|
||||||
|
list.addAll(origin)
|
||||||
|
}
|
||||||
|
list += value
|
||||||
|
result.put(name, result, list.toNativeArray())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import org.autojs.autojs.AutoJs
|
import org.autojs.autojs.AutoJs
|
||||||
|
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
|
||||||
import org.autojs.autojs.core.automator.UiObjectCollection
|
import org.autojs.autojs.core.automator.UiObjectCollection
|
||||||
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
|
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
|
||||||
import org.autojs.autojs.extension.AnyExtensions.jsBrief
|
import org.autojs.autojs.extension.AnyExtensions.jsBrief
|
||||||
@@ -50,6 +51,7 @@ import org.mozilla.javascript.Wrapper
|
|||||||
import org.mozilla.javascript.json.JsonParser
|
import org.mozilla.javascript.json.JsonParser
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
import java.lang.reflect.Method
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -909,6 +911,13 @@ object RhinoUtils {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun Class<*>.getRhinoStandardFunctionMethods(): List<Method> {
|
||||||
|
return this.declaredMethods.filter {
|
||||||
|
it.isAnnotationPresent(RhinoStandardFunctionInterface::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ObsoletedRhinoFunctionException(funcName: String) : Exception(
|
class ObsoletedRhinoFunctionException(funcName: String) : Exception(
|
||||||
"Function \"$funcName\" can no longer be used as it has been obsoleted",
|
"Function \"$funcName\" can no longer be used as it has been obsoleted",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Fri Dec 26 16:11:42 CST 2025
|
#Sat Dec 27 13:20:38 CST 2025
|
||||||
BUILD_TIME=1766736702142
|
BUILD_TIME=1766812838968
|
||||||
COMPILE_SDK_VERSION=36
|
COMPILE_SDK_VERSION=36
|
||||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
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
|
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||||
TARGET_SDK_VERSION=36
|
TARGET_SDK_VERSION=36
|
||||||
TARGET_SDK_VERSION_INRT=29
|
TARGET_SDK_VERSION_INRT=29
|
||||||
VERSION_BUILD=3551
|
VERSION_BUILD=3554
|
||||||
VERSION_NAME=6.7.0 Alpha13
|
VERSION_NAME=6.7.0 Alpha13
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
||||||
|
|||||||
Reference in New Issue
Block a user