diff --git a/lib/src/main/java/com/example/service/BaseService.kt b/lib/src/main/java/com/example/service/BaseService.kt new file mode 100644 index 0000000..686b2af --- /dev/null +++ b/lib/src/main/java/com/example/service/BaseService.kt @@ -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) -> 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.replaceVariableData(): List { + return map { + NameValue( + it.name.toVariableData(), + it.value.toVariableData() + ) + } + } + + // ==================== Cookie 处理 ==================== + + protected fun cookieFromCookieManager(url: String): Set { + val cookiesCache = mutableSetOf() + + runCatching { + val urlObj = URL(url) + val uri = URI(urlObj.protocol, urlObj.host, urlObj.path, urlObj.query, null) + val headers = mutableMapOf>() + + 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.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.nameValueToMap(): Map = associate { + it.name to it.value + } + + protected fun List.amendBaseHeader( + includeAccept: Boolean + ): List { + val headers = toMutableList() + + if (headers.isEmpty()) { + headers.addDefaultHeaders(includeAccept) + } else { + headers.ensureDefaultHeaders(includeAccept) + } + + return headers + } + + private fun MutableList.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.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.addSecChUa(): MutableList { + 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.hasHeader(headerName: String): Boolean { + return any { it.name.contentEquals(headerName, ignoreCase = true) } + } + + protected fun Request.genBaseHeaderMap(): Map = 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? { + 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.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 + } +} \ No newline at end of file diff --git a/lib/src/main/java/com/example/service/HttpService.kt b/lib/src/main/java/com/example/service/HttpService.kt new file mode 100644 index 0000000..3aa8ee7 --- /dev/null +++ b/lib/src/main/java/com/example/service/HttpService.kt @@ -0,0 +1,4 @@ +package com.example.service + +class HttpService { +} \ No newline at end of file