parent
9934938dac
commit
63ab06eefa
@ -0,0 +1,329 @@
|
||||
package com.example.service
|
||||
|
||||
import android.os.Build
|
||||
import com.example.action.BaseAction
|
||||
import com.example.action.NameValue
|
||||
import com.example.action.Next
|
||||
import com.example.action.VarExtractRule
|
||||
import com.example.http.HttpClient
|
||||
import com.example.logger.LogUtils
|
||||
import com.example.report.ActionExec
|
||||
import com.example.task.TaskConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.json.JSONArray
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import java.util.Base64
|
||||
import com.example.http.HttpClient.Params
|
||||
import com.example.http.Request
|
||||
import com.example.utils.WebSocketUtil
|
||||
import com.example.utils.toJsonString
|
||||
|
||||
abstract class BaseService(
|
||||
protected open val taskConfig: TaskConfig,
|
||||
protected val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
) {
|
||||
|
||||
abstract suspend fun execute(onFinish: (List<ActionExec>) -> Unit)
|
||||
|
||||
companion object {
|
||||
// HTTP 错误码
|
||||
const val ERROR_CODE_HTTP_ACTION_EXEC_FAILED = 600
|
||||
const val ERROR_CODE_HTTP_BUILD_REDIRECT_FAILED = 615
|
||||
|
||||
// PIN 动作错误码
|
||||
const val ERROR_CODE_PIN_ACTION_EXEC_FAILED = 700
|
||||
const val ERROR_CODE_PIN_ACTION_TIMEOUT = 702
|
||||
|
||||
// WebSocket 错误码
|
||||
const val WEB_SOCKET_CODE = 101
|
||||
const val ERROR_CODE_WS_ACTION_EXEC_FAILED = 800
|
||||
|
||||
// 通用异步执行错误码
|
||||
const val ASYNC_EXEC_CODE = 900
|
||||
}
|
||||
|
||||
// ==================== 变量处理 ====================
|
||||
|
||||
protected fun String.toVariableData(): String {
|
||||
if (isBlank()) return this
|
||||
|
||||
var result = this
|
||||
taskConfig.variableCache.forEach { (key, value) ->
|
||||
if (value.isNotBlank()) {
|
||||
val placeholder = "{$key}"
|
||||
result = result.replace(placeholder, value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
protected fun List<NameValue>.replaceVariableData(): List<NameValue> {
|
||||
return map {
|
||||
NameValue(
|
||||
it.name.toVariableData(),
|
||||
it.value.toVariableData()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Cookie 处理 ====================
|
||||
|
||||
protected fun cookieFromCookieManager(url: String): Set<NameValue> {
|
||||
val cookiesCache = mutableSetOf<NameValue>()
|
||||
|
||||
runCatching {
|
||||
val urlObj = URL(url)
|
||||
val uri = URI(urlObj.protocol, urlObj.host, urlObj.path, urlObj.query, null)
|
||||
val headers = mutableMapOf<String, List<String>>()
|
||||
|
||||
taskConfig.cookieManager.get(uri, headers)?.let { responseHeaders ->
|
||||
val cookieList = responseHeaders.getOrElse(Params.REQUEST_HEADER_COOKIE) { emptyList() }
|
||||
val cookies = cookieList.firstOrNull()
|
||||
|
||||
if (!cookies.isNullOrBlank()) {
|
||||
cookies.split("; ").forEach { cookie ->
|
||||
cookie.split("=", limit = 2).takeIf { it.size >= 2 }?.let { kv ->
|
||||
cookiesCache += NameValue(kv[0], kv[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.info("getCookie $url : $cookiesCache")
|
||||
return cookiesCache
|
||||
}
|
||||
|
||||
protected fun List<NameValue>.buildCookie() = NameValue(
|
||||
name = Params.REQUEST_HEADER_COOKIE,
|
||||
value = joinToString("; ") { "${it.name}=${it.value}" }
|
||||
)
|
||||
|
||||
// ==================== URL 处理 ====================
|
||||
|
||||
protected fun String.urlEncode(): String? = runCatching {
|
||||
URLEncoder.encode(this, "UTF-8")
|
||||
}.getOrNull()
|
||||
|
||||
protected fun String.isHttps(): Boolean = startsWith("https", ignoreCase = true)
|
||||
|
||||
// ==================== Header 处理 ====================
|
||||
|
||||
protected fun List<NameValue>.nameValueToMap(): Map<String, String> = associate {
|
||||
it.name to it.value
|
||||
}
|
||||
|
||||
protected fun List<NameValue>.amendBaseHeader(
|
||||
includeAccept: Boolean
|
||||
): List<NameValue> {
|
||||
val headers = toMutableList()
|
||||
|
||||
if (headers.isEmpty()) {
|
||||
headers.addDefaultHeaders(includeAccept)
|
||||
} else {
|
||||
headers.ensureDefaultHeaders(includeAccept)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private fun MutableList<NameValue>.addDefaultHeaders(includeAccept: Boolean) {
|
||||
add(NameValue(Params.REQUEST_HEADER_USER_AGENT, taskConfig.userAgent))
|
||||
if (includeAccept) {
|
||||
add(NameValue(Params.REQUEST_HEADER_ACCEPT, taskConfig.accept))
|
||||
}
|
||||
add(NameValue(Params.REQUEST_HEADER_ACCEPT_LANGUAGE, taskConfig.acceptLanguage))
|
||||
}
|
||||
|
||||
private fun MutableList<NameValue>.ensureDefaultHeaders(includeAccept: Boolean) {
|
||||
if (!hasHeader(Params.REQUEST_HEADER_USER_AGENT)) {
|
||||
add(NameValue(Params.REQUEST_HEADER_USER_AGENT, taskConfig.userAgent))
|
||||
}
|
||||
if (includeAccept && !hasHeader(Params.REQUEST_HEADER_ACCEPT)) {
|
||||
add(NameValue(Params.REQUEST_HEADER_ACCEPT, taskConfig.accept))
|
||||
}
|
||||
if (!hasHeader(Params.REQUEST_HEADER_ACCEPT_LANGUAGE)) {
|
||||
add(NameValue(Params.REQUEST_HEADER_ACCEPT_LANGUAGE, taskConfig.acceptLanguage))
|
||||
}
|
||||
}
|
||||
|
||||
protected fun List<NameValue>.addSecChUa(): MutableList<NameValue> {
|
||||
val headers = toMutableList()
|
||||
|
||||
if (!headers.hasHeader(Params.REQUEST_HEADER_SEC_CH_UA)) {
|
||||
headers.add(NameValue(
|
||||
Params.REQUEST_HEADER_SEC_CH_UA,
|
||||
taskConfig.secChUa.ifBlank { "\"\"" }
|
||||
))
|
||||
}
|
||||
if (!headers.hasHeader(Params.REQUEST_HEADER_SEC_CH_UA_MOBILE)) {
|
||||
headers.add(NameValue(Params.REQUEST_HEADER_SEC_CH_UA_MOBILE, "?1"))
|
||||
}
|
||||
if (!headers.hasHeader(Params.REQUEST_HEADER_SEC_CH_UA_PLATFORM)) {
|
||||
headers.add(NameValue(Params.REQUEST_HEADER_SEC_CH_UA_PLATFORM, "\"Android\""))
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private fun List<NameValue>.hasHeader(headerName: String): Boolean {
|
||||
return any { it.name.contentEquals(headerName, ignoreCase = true) }
|
||||
}
|
||||
|
||||
protected fun Request.genBaseHeaderMap(): Map<String, String> = headers.filterKeys {
|
||||
Params.REQUEST_HEADER_USER_AGENT.equals(it, true) ||
|
||||
Params.REQUEST_HEADER_ACCEPT_LANGUAGE.equals(it, true) ||
|
||||
Params.REQUEST_HEADER_ACCEPT.equals(it, true)
|
||||
}
|
||||
|
||||
// ==================== 变量提取 ====================
|
||||
|
||||
protected fun extractBodyVariableToCache(
|
||||
action: BaseAction,
|
||||
wholeResponseData: String,
|
||||
responseBody: ByteArray
|
||||
) = runCatching {
|
||||
val params = getExtractParams(action) ?: return@runCatching
|
||||
|
||||
params.forEach { extractRule ->
|
||||
val value = extractValue(extractRule, wholeResponseData, responseBody)
|
||||
taskConfig.variableCache[extractRule.variable] = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExtractParams(action: BaseAction): List<VarExtractRule>? {
|
||||
return when (action) {
|
||||
is BaseAction.HttpAction -> action.response?.params
|
||||
is BaseAction.PinAction -> action.params
|
||||
is BaseAction.WebSocketAction -> action.response?.params
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractValue(
|
||||
extractRule: VarExtractRule,
|
||||
wholeResponseData: String,
|
||||
responseBody: ByteArray
|
||||
): String {
|
||||
return when (extractRule) {
|
||||
is VarExtractRule.Base64 -> encodeBase64(responseBody)
|
||||
is VarExtractRule.Between -> extractBetween(extractRule.expr, wholeResponseData)
|
||||
is VarExtractRule.Regexp -> extractRegex(extractRule.expr, wholeResponseData)
|
||||
is VarExtractRule.Response -> String(responseBody)
|
||||
is VarExtractRule.Whole -> wholeResponseData
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeBase64(data: ByteArray): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Base64.getEncoder().encodeToString(data)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
android.util.Base64.encodeToString(data, android.util.Base64.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractBetween(expr: String, data: String): String {
|
||||
val parts = expr.split("|").take(2)
|
||||
if (parts.size < 2) return ""
|
||||
return data.substringAfter(parts[0], missingDelimiterValue = "")
|
||||
.substringBefore(parts[1], missingDelimiterValue = "")
|
||||
}
|
||||
|
||||
private fun extractRegex(pattern: String, data: String): String {
|
||||
return Regex(pattern).find(data)?.groupValues?.getOrNull(1) ?: ""
|
||||
}
|
||||
|
||||
// ==================== 错误处理 ====================
|
||||
|
||||
protected fun genExceptionActionExec(
|
||||
action: BaseAction,
|
||||
errorCode: Int,
|
||||
exceptionMessage: String
|
||||
): ActionExec {
|
||||
return ActionExec().apply {
|
||||
step = taskConfig.currentStep
|
||||
index = 1
|
||||
time = System.currentTimeMillis()
|
||||
respCode = errorCode
|
||||
cost = 0
|
||||
|
||||
fillActionDetails(action, exceptionMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ActionExec.fillActionDetails(action: BaseAction, exceptionMessage: String) {
|
||||
when (action) {
|
||||
is BaseAction.HttpAction -> {
|
||||
action.request?.let { request ->
|
||||
url = request.url
|
||||
method = request.method.value
|
||||
reqHeader = request.headers.toJsonString()
|
||||
reqData = request.data.takeIf { it.isNotBlank() } ?: exceptionMessage
|
||||
} ?: run {
|
||||
reqData = exceptionMessage
|
||||
}
|
||||
}
|
||||
|
||||
is BaseAction.PinAction -> {
|
||||
// PinAction 特定处理可以在这里添加
|
||||
}
|
||||
|
||||
is BaseAction.WebSocketAction -> {
|
||||
action.request?.let { request ->
|
||||
url = request.url
|
||||
method = WebSocketUtil.WEB_SOCKET_REQUEST_METHOD
|
||||
reqHeader = request.headers.toJsonString()
|
||||
|
||||
if (request.params.isNotEmpty()) {
|
||||
val messages = JSONArray().apply {
|
||||
request.params.forEach { param ->
|
||||
put(param.value)
|
||||
}
|
||||
}
|
||||
reqData = messages.toString()
|
||||
} else {
|
||||
reqData = exceptionMessage
|
||||
}
|
||||
} ?: run {
|
||||
reqData = exceptionMessage
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
LogUtils.info("unknown action type: ${action::class.java.simpleName}")
|
||||
reqData = exceptionMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 步骤控制 ====================
|
||||
|
||||
protected fun List<Next>.getNextStepIndex(
|
||||
wholeResponse: String,
|
||||
currentStep: Int
|
||||
): Int {
|
||||
if (isEmpty() || wholeResponse.isBlank()) {
|
||||
return currentStep + 1
|
||||
}
|
||||
|
||||
filter { it.step > 0 }.forEach { next ->
|
||||
// 检查包含匹配
|
||||
if (next.contain.isNotBlank() && wholeResponse.contains(next.contain)) {
|
||||
return next.step
|
||||
}
|
||||
|
||||
// 检查正则匹配
|
||||
if (next.regexp.isNotBlank()) {
|
||||
Regex(next.regexp).find(wholeResponse)?.let {
|
||||
return next.step
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package com.example.service
|
||||
|
||||
class HttpService {
|
||||
}
|
||||
Loading…
Reference in new issue