From fbc899a8249b86defd9574478b3d46003f3b2a53 Mon Sep 17 00:00:00 2001 From: mojo Date: Wed, 5 Nov 2025 15:24:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=20HttpService=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E5=92=8C=E6=94=B9=E8=BF=9B=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E8=B4=A8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/service/HttpService.kt | 646 +++++++++++------- 1 file changed, 381 insertions(+), 265 deletions(-) diff --git a/lib/src/main/java/com/example/service/HttpService.kt b/lib/src/main/java/com/example/service/HttpService.kt index 4eb8c88..3f7e246 100644 --- a/lib/src/main/java/com/example/service/HttpService.kt +++ b/lib/src/main/java/com/example/service/HttpService.kt @@ -19,7 +19,6 @@ import com.example.utils.toJsonString import com.example.utils.toJsonString1 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.net.URI @@ -34,136 +33,220 @@ class HttpService( companion object { private const val MAX_REDIRECT_COUNT = 30 - val pattern = Regex("^[a-zA-Z0-9]+://.*") + private val HTTP_REDIRECT_PATTERN = Regex("^[a-zA-Z0-9]+://.*") + private val HTTP_STATUS_SUCCESS = 100 until 300 + private val HTTP_STATUS_REDIRECT = 300 until 400 } + // ==================== 主执行方法 ==================== + override suspend fun execute(onFinish: (List) -> Unit) = withContext(Dispatchers.Default) { val actionExecList = mutableListOf() - var currentStep = taskConfig.currentStep + val currentStep = taskConfig.currentStep + try { - LogUtils.info("action delay: ${action.delay} s, it's async: ${action.async}") - if (action.delay > 0) { - delay(action.delay.toDuration(DurationUnit.SECONDS)) - } + handleActionDelay() val actionRequest = action.request ?: throw NullPointerException("request is null") + amendActionRequest(actionRequest) - var httpRequest = actionRequest.buildHttpRequest() - var httpResponse: Response? = null - var proceedTask = false - var stepCallHttpCount = 1 - if (action.async) { - httpRequest.makeAsyncRequest() - val actionExec = httpRequest.genActionExec(null, stepCallHttpCount) - actionExec.respCode = ASYNC_EXEC_CODE - actionExecList += actionExec - proceedTask = true - } else { - httpResponse = httpRequest.call() - val actionExec = httpRequest.genActionExec(httpResponse, stepCallHttpCount) - actionExecList += actionExec - when (httpResponse.code) { - in 100 until 300 -> { - proceedTask = true - } - - in 300 until 400 -> { - var redirectUrl: String? = - httpResponse.headers.get( - HttpClient.Params.REQUEST_HEADER_LOCATION, - ignoreCase = true - )?.firstOrNull() - while ((isActive && httpResponse != null && - httpResponse.code in (300 until 400) && - stepCallHttpCount <= MAX_REDIRECT_COUNT && - !redirectUrl.isNullOrBlank()) && - redirectUrl.isHttpRedirect() - ) { - runCatching { - stepCallHttpCount++ - httpRequest = httpRequest.buildRedirectHttpRequest( - redirectUrl!!, - httpRequest.url - ) - httpResponse = httpRequest.call() - if (httpResponse!!.code !in 300 until 400) { - if (!httpResponse?.headers.isNullOrEmpty()) { - httpResponse?.headers = - httpResponse!!.headers.toMutableMap().apply { - put( - HttpClient.Params.REQUEST_HEADER_REFERER, - listOf( - httpRequest.headers.getOrDefault( - HttpClient.Params.REQUEST_HEADER_REFERER, - ignoreCase = true, - "" - ) - ) - ) - } - } - } - - redirectUrl = - httpResponse?.headers?.get( - HttpClient.Params.REQUEST_HEADER_LOCATION, - ignoreCase = true - )?.firstOrNull() - LogUtils.info("redirectUrl: $redirectUrl") - proceedTask = true - httpRequest.genActionExec(httpResponse, stepCallHttpCount) - }.onFailure { e -> - LogUtils.error(throwable = e) - proceedTask = action.skipError - actionExecList += httpRequest.genActionExec( - null, stepCallHttpCount - ).apply { - respCode = - HttpClient.ErrorCode.ERROR_CODE_HTTP_BUILD_CONNECTION_FAILED - } - }.onSuccess { - actionExecList += it - } - } - } - - else -> { - proceedTask = action.skipError - } - } - } - - if (proceedTask) { - httpResponse?.apply { - extractResponseVariableToCache(action, httpRequest, httpResponse) - val nextStep = action.next.httpGetNextStepIndex( - httpRequest, httpResponse, currentStep - ) - taskConfig.currentStep = nextStep - } ?: let { - taskConfig.currentStep = ++currentStep - } + val httpRequest = actionRequest.buildHttpRequest() + + val result = if (action.async) { + handleAsyncRequest(httpRequest, actionExecList) } else { - taskConfig.currentStep = Int.MAX_VALUE + handleSyncRequest(httpRequest, actionExecList) } + + updateTaskStep(result, currentStep, httpRequest) } catch (e: Exception) { LogUtils.error(throwable = e) - val actionExec = genExceptionActionExec( - action, ERROR_CODE_HTTP_ACTION_EXEC_FAILED, Log.getStackTraceString(e) - ) - actionExecList += actionExec - if (action.skipError) { - taskConfig.currentStep = ++currentStep - } else { - taskConfig.currentStep = Int.MAX_VALUE - } + handleException(e, actionExecList, currentStep) } + LogUtils.info("finish action: ${action.request?.url}") onFinish(actionExecList) } + // ==================== 请求处理 ==================== + + private suspend fun handleActionDelay() { + if (action.delay > 0) { + LogUtils.info("action delay: ${action.delay} s, it's async: ${action.async}") + delay(action.delay.toDuration(DurationUnit.SECONDS)) + } + } + + private data class RequestResult( + val proceedTask: Boolean, + val httpResponse: Response? + ) + + private fun handleAsyncRequest( + httpRequest: Request, + actionExecList: MutableList + ): RequestResult { + httpRequest.makeAsyncRequest() + val actionExec = httpRequest.genActionExec(null, 1) + actionExec.respCode = ASYNC_EXEC_CODE + actionExecList += actionExec + return RequestResult(proceedTask = true, httpResponse = null) + } + + private suspend fun handleSyncRequest( + httpRequest: Request, + actionExecList: MutableList + ): RequestResult { + val request = httpRequest + val response = request.call() + val stepCallHttpCount = 1 + + val actionExec = request.genActionExec(response, stepCallHttpCount) + actionExecList += actionExec + + return when (response.code) { + in HTTP_STATUS_SUCCESS -> { + RequestResult(proceedTask = true, httpResponse = response) + } + + in HTTP_STATUS_REDIRECT -> { + handleRedirects(request, response, actionExecList).let { redirectResult -> + RequestResult( + proceedTask = redirectResult.proceedTask, + httpResponse = redirectResult.response + ) + } + } + + else -> { + RequestResult(proceedTask = action.skipError, httpResponse = response) + } + } + } + + private data class RedirectResult( + val proceedTask: Boolean, + val response: Response? + ) + + private suspend fun handleRedirects( + initialRequest: Request, + initialResponse: Response, + actionExecList: MutableList + ): RedirectResult { + var request = initialRequest + var response = initialResponse + var stepCallHttpCount = 1 + var redirectUrl = getRedirectUrl(response) + + while (shouldContinueRedirect(response, redirectUrl, stepCallHttpCount)) { + stepCallHttpCount++ + + runCatching { + request = request.buildRedirectHttpRequest(redirectUrl!!, request.url) + response = request.call() + + if (response.code !in HTTP_STATUS_REDIRECT) { + updateResponseReferer(request, response) + } + + redirectUrl = getRedirectUrl(response) + LogUtils.info("redirectUrl: $redirectUrl") + + request.genActionExec(response, stepCallHttpCount) + }.onFailure { e -> + LogUtils.error(throwable = e) + val errorExec = request.genActionExec(null, stepCallHttpCount).apply { + respCode = HttpClient.ErrorCode.ERROR_CODE_HTTP_BUILD_CONNECTION_FAILED + } + actionExecList += errorExec + return RedirectResult(proceedTask = action.skipError, response = null) + }.onSuccess { actionExec -> + actionExecList += actionExec + } + } + + return RedirectResult(proceedTask = true, response = response) + } + + private fun shouldContinueRedirect( + response: Response?, + redirectUrl: String?, + stepCallHttpCount: Int + ): Boolean { + return response != null && + response.code in HTTP_STATUS_REDIRECT && + stepCallHttpCount <= MAX_REDIRECT_COUNT && + !redirectUrl.isNullOrBlank() && + redirectUrl.isHttpRedirect() + } + + private fun getRedirectUrl(response: Response?): String? { + return response?.headers?.get( + HttpClient.Params.REQUEST_HEADER_LOCATION, + ignoreCase = true + )?.firstOrNull() + } + + private fun updateResponseReferer(request: Request, response: Response) { + if (response.headers.isNotEmpty()) { + response.headers = response.headers.toMutableMap().apply { + put( + HttpClient.Params.REQUEST_HEADER_REFERER, + listOf( + request.headers.getOrDefault( + HttpClient.Params.REQUEST_HEADER_REFERER, + ignoreCase = true, + "" + ) + ) + ) + } + } + } + + private fun updateTaskStep( + result: RequestResult, + currentStep: Int, + httpRequest: Request + ) { + if (result.proceedTask) { + result.httpResponse?.let { response -> + extractResponseVariableToCache(action, httpRequest, response) + val nextStep = action.next.httpGetNextStepIndex( + httpRequest, response, currentStep + ) + taskConfig.currentStep = nextStep + } ?: run { + taskConfig.currentStep = currentStep + 1 + } + } else { + taskConfig.currentStep = Int.MAX_VALUE + } + } + + private fun handleException( + e: Exception, + actionExecList: MutableList, + currentStep: Int + ) { + val actionExec = genExceptionActionExec( + action, ERROR_CODE_HTTP_ACTION_EXEC_FAILED, Log.getStackTraceString(e) + ) + actionExecList += actionExec + + taskConfig.currentStep = if (action.skipError) { + currentStep + 1 + } else { + Int.MAX_VALUE + } + } + + // ==================== ActionExec 生成 ==================== + private fun Request.genActionExec( - httpResponse: Response?, redirectCount: Int + httpResponse: Response?, + redirectCount: Int ): ActionExec { val actionExec = ActionExec( step = taskConfig.currentStep, @@ -179,66 +262,69 @@ class HttpService( } httpResponse?.let { response -> - if (response.headers.isNotEmpty()) { - kotlin.runCatching { - URL(url).apply { - URI(protocol, host, path, query, null).let { uri -> - taskConfig.cookieManager.put(uri, response.headers) - } - } - }.onFailure { - LogUtils.error(throwable = it) - } - actionExec.respHeader = response.headers.toJsonString() - } + saveCookiesFromResponse(response, url) + fillResponseData(actionExec, response) + } + + return actionExec + } - actionExec.respCode = response.code - if (response.data.isNotEmpty()) { - actionExec.respData = String(response.data) + private fun saveCookiesFromResponse(response: Response, url:String) { + if (response.headers.isEmpty()) return + + runCatching { + URL(url).apply { + val uri = URI(protocol, host, path, query, null) + taskConfig.cookieManager.put(uri, response.headers) } - actionExec.cost = httpResponse.endTime - httpResponse.startTime + }.onFailure { + LogUtils.error(throwable = it) + } + } + private fun fillResponseData(actionExec: ActionExec, response: Response) { + if (response.headers.isNotEmpty()) { + actionExec.respHeader = response.headers.toJsonString() } - return actionExec + + actionExec.respCode = response.code + + if (response.data.isNotEmpty()) { + actionExec.respData = String(response.data) + } + + actionExec.cost = response.endTime - response.startTime } + // ==================== 异步请求处理 ==================== + private fun Request.makeAsyncRequest() = scope.launch { + var request = copy() + var response = request.call() var stepCallHttpCount = 0 - var asyncRequest: Request = copy() - var asyncResponse = asyncRequest.call() - var locationList: MutableList - var redirectUrl: String? = null - if (asyncResponse.code in 300 until 400) { - locationList = - asyncResponse.headers.get( - HttpClient.Params.REQUEST_HEADER_LOCATION, - ignoreCase = true - )?.toMutableList() - ?: mutableListOf() - redirectUrl = locationList.firstOrNull() - } - while (asyncResponse.code in 300 until 400 && stepCallHttpCount <= MAX_REDIRECT_COUNT && !redirectUrl.isNullOrBlank() && redirectUrl.isHttpRedirect()) { - kotlin.runCatching { - stepCallHttpCount++ - asyncRequest = - asyncRequest.buildRedirectHttpRequest(redirectUrl!!, asyncRequest.url) - asyncResponse = asyncRequest.call() - locationList = - asyncResponse.headers.get( - HttpClient.Params.REQUEST_HEADER_LOCATION, - ignoreCase = true - )?.toMutableList() - ?: mutableListOf() - redirectUrl = locationList.firstOrNull() + var redirectUrl = getRedirectUrl(response) + + while (shouldContinueRedirect(response, redirectUrl, stepCallHttpCount)) { + stepCallHttpCount++ + + runCatching { + request = request.buildRedirectHttpRequest(redirectUrl!!, request.url) + response = request.call() + redirectUrl = getRedirectUrl(response) }.onFailure { LogUtils.error(throwable = it) + return@launch } } } - private fun String.isHttpRedirect(): Boolean = pattern.find(this)?.let { - return@isHttpRedirect this.startsWith("http") - } ?: true + // ==================== URL 处理 ==================== + + private fun String.isHttpRedirect(): Boolean { + return HTTP_REDIRECT_PATTERN.find(this)?.let { + startsWith("http", ignoreCase = true) + } ?: true + } private fun HttpActionRequest.buildHttpRequest(): Request = Request( url = url, @@ -247,193 +333,223 @@ class HttpService( headers = headers.nameValueToMap() ) - private fun genWholeResponse(httpRequest: Request, httpResponse: Response?): String { - return """ - [${httpRequest.url}]${if (httpResponse?.data?.isNotEmpty() == true) String(httpResponse.data) else ""}[${ - httpResponse?.headers?.let { - if (it.isEmpty()) "" - else it.toJsonString() - } - }] - """.trimIndent() + val responseData = httpResponse?.data?.takeIf { it.isNotEmpty() }?.let { + String(it) + } ?: "" + + val responseHeaders = httpResponse?.headers?.takeIf { it.isNotEmpty() }?.toJsonString() ?: "" + + return "[${httpRequest.url}]$responseData[$responseHeaders]" } + // ==================== 步骤控制 ==================== + private fun List.httpGetNextStepIndex( - httpRequest: Request, httpResponse: Response?, currentStep: Int + httpRequest: Request, + httpResponse: Response?, + currentStep: Int ): Int { val wholeResponse = genWholeResponse(httpRequest, httpResponse) return getNextStepIndex(wholeResponse, currentStep) } + // ==================== 变量提取 ==================== + private fun extractResponseVariableToCache( - action: BaseAction.HttpAction, httpRequest: Request, httpResponse: Response? + action: BaseAction.HttpAction, + httpRequest: Request, + httpResponse: Response ) { - action.response?.let { actionResponse -> - httpResponse?.headers?.let { responseHeaders -> - extractCookieVariableToCache(actionResponse.cookies, responseHeaders) - extractHeaderVariableToCache(actionResponse.headers, responseHeaders) - extractBodyVariableToCache( - action, genWholeResponse(httpRequest, httpResponse), httpResponse.data - ) - } - } + val actionResponse = action.response ?: return + val responseHeaders = httpResponse.headers + + extractCookieVariableToCache(actionResponse.cookies, responseHeaders) + extractHeaderVariableToCache(actionResponse.headers, responseHeaders) + extractBodyVariableToCache( + action, + genWholeResponse(httpRequest, httpResponse), + httpResponse.data + ) } private fun extractCookieVariableToCache( - cookies: List, responseHeaders: Map> + cookies: List, + responseHeaders: Map> ) { if (cookies.isEmpty()) return + runCatching { - val cookieList = - responseHeaders.get(HttpClient.Params.RESPONSE_HEADER_SET_COOKIE, ignoreCase = true) - if (cookieList.isNullOrEmpty()) return - cookies.map { nameVariable -> - cookieList.map { cookie -> - val cookieValues = cookie.split(";") - cookieValues.map { cookieValue -> - val keyPair = cookieValue.split("=", limit = 2) - val key = keyPair.first().trim() - val value = keyPair.getOrElse(1) { "" }.trim() - if (key == nameVariable.name) { - taskConfig.variableCache[nameVariable.variable] = value - } + val cookieList = responseHeaders.get( + HttpClient.Params.RESPONSE_HEADER_SET_COOKIE, + ignoreCase = true + ) ?: return + + cookies.forEach { nameVariable -> + cookieList.forEach { cookie -> + parseCookieValue(cookie, nameVariable.name)?.let { value -> + taskConfig.variableCache[nameVariable.variable] = value } } } - }.onFailure { LogUtils.error(throwable = it) } } + private fun parseCookieValue(cookie: String, targetName: String): String? { + val cookieValues = cookie.split(";") + cookieValues.forEach { cookieValue -> + val keyPair = cookieValue.split("=", limit = 2) + val key = keyPair.first().trim() + val value = keyPair.getOrElse(1) { "" }.trim() + + if (key == targetName) { + return value + } + } + return null + } + private fun extractHeaderVariableToCache( - headers: List, responseHeaders: Map> + headers: List, + responseHeaders: Map> ) { if (headers.isEmpty() || responseHeaders.isEmpty()) return - headers.map { nameVariable -> - responseHeaders[nameVariable.name]?.firstOrNull()?.apply { - taskConfig.variableCache[nameVariable.variable] = this + + headers.forEach { nameVariable -> + responseHeaders[nameVariable.name]?.firstOrNull()?.let { value -> + taskConfig.variableCache[nameVariable.variable] = value } - } } + // ==================== 请求构建 ==================== + private fun amendActionRequest(actionRequest: HttpActionRequest) { + // 替换变量 actionRequest.headers.replaceVariableData() actionRequest.cookies.replaceVariableData() actionRequest.params.replaceVariableData() - - actionRequest.headers.amendBaseHeader(true).apply { - actionRequest.headers = this.toMutableList() - } - - if (actionRequest.data.isNotBlank()) { actionRequest.data = actionRequest.data.toVariableData() } - + + // 添加基础 Header + actionRequest.headers = actionRequest.headers.amendBaseHeader(true).toMutableList() + + // 处理 URL actionRequest.url = when (actionRequest.method) { - HttpMethod.Get -> { - buildGetUrl(actionRequest) - } - - else -> { - actionRequest.url.toVariableData() - } + HttpMethod.Get -> buildGetUrl(actionRequest) + else -> actionRequest.url.toVariableData() } - - if (actionRequest.url.startsWith("https", ignoreCase = true)) { - actionRequest.headers.addSecChUa().apply { - actionRequest.headers = this - } + + // 添加 Sec-CH-UA Header (HTTPS) + if (actionRequest.url.isHttps()) { + actionRequest.headers = actionRequest.headers.addSecChUa() } - + + // 处理 Cookie actionRequest.amendCookie() - if (HttpMethod.Get != actionRequest.method) { - val sendData: ByteArray = actionRequest.genPostData() - if (sendData.isNotEmpty()) actionRequest.data = String(sendData) + + // 处理 POST 数据 + if (actionRequest.method != HttpMethod.Get) { + val sendData = actionRequest.genPostData() + if (sendData.isNotEmpty()) { + actionRequest.data = String(sendData) + } } } private fun buildGetUrl(actionRequest: HttpActionRequest): String { - return actionRequest.url.toVariableData().let { u -> - Uri.parse(u).buildUpon().apply { - actionRequest.params.forEach { nameValue -> - this.appendQueryParameter(nameValue.name, nameValue.value) - } + val baseUrl = actionRequest.url.toVariableData() + return baseUrl.toUri().buildUpon().apply { + actionRequest.params.forEach { nameValue -> + appendQueryParameter(nameValue.name, nameValue.value) } }.build().toString() } private fun HttpActionRequest.amendCookie() { - val cookies: MutableSet = mutableSetOf() - cookieFromCookieManager(url).apply { - if (autoCookie && isNotEmpty()) { - cookies += this - } + val cookies = mutableSetOf() + + // 从 CookieManager 获取 Cookie + if (autoCookie) { + cookies += cookieFromCookieManager(url) } - this.cookies.apply { - if (isNotEmpty()) { - forEach { - cookies.remove(it) - cookies += it - } - } + + // 添加手动指定的 Cookie(覆盖同名 Cookie) + this.cookies.forEach { + cookies.remove(it) + cookies += it } + + // 构建 Cookie Header if (cookies.isNotEmpty()) { headers += cookies.toList().buildCookie() } } private fun HttpActionRequest.genPostData(): ByteArray { - var sendData: ByteArray = byteArrayOf() + // 优先使用 params(form-data) if (params.isNotEmpty()) { - sendData = params.joinToString("&") { + return params.joinToString("&") { "${it.name.urlEncode()}=${it.value.urlEncode()}" }.toByteArray() } - + + // 否则使用 data(raw data) if (data.isNotBlank()) { - sendData = data.toByteArray(Charsets.UTF_8) + return data.toByteArray(Charsets.UTF_8) } - return sendData + + return byteArrayOf() } - private fun Request.buildRedirectHttpRequest(redirectUrl: String, originUrl: String): Request { - val headers = this.genBaseHeaderMap().toMutableMap() - headers[HttpClient.Params.REQUEST_HEADER_REFERER] = originUrl - - - val request = Request( - url = URL(URL(originUrl), redirectUrl.replace(" ", "%20")).toString(), - method = HttpMethod.Get, - headers = headers - ) + // ==================== 重定向请求构建 ==================== - val cookies = cookieFromCookieManager(request.url) + private fun Request.buildRedirectHttpRequest( + redirectUrl: String, + originUrl: String + ): Request { + val headers = genBaseHeaderMap().toMutableMap() + headers[HttpClient.Params.REQUEST_HEADER_REFERER] = originUrl + + val normalizedUrl = URL(URL(originUrl), redirectUrl.replace(" ", "%20")).toString() + + // 添加 Cookie + val cookies = cookieFromCookieManager(normalizedUrl) if (cookies.isNotEmpty()) { cookies.toList().buildCookie().let { cookie -> - headers.put(cookie.name, cookie.value) + headers[cookie.name] = cookie.value } } - - if (request.url.isHttps()) { - val secChUa = mutableListOf().addSecChUa() - secChUa.forEach { + + // 添加 Sec-CH-UA Header (HTTPS) + if (normalizedUrl.isHttps()) { + mutableListOf().addSecChUa().forEach { headers[it.name] = it.value } } - - return request + + return Request( + url = normalizedUrl, + method = HttpMethod.Get, + headers = headers + ) } } +// ==================== Map 扩展函数 ==================== + fun Map.get(key: String, ignoreCase: Boolean): V? { - return this.entries.firstOrNull { + return entries.firstOrNull { it.key.equals(key, ignoreCase = ignoreCase) }?.value } -fun Map.getOrDefault(key: String, ignoreCase: Boolean, defaultValue: V): V = - this.get(key, ignoreCase) ?: defaultValue \ No newline at end of file +fun Map.getOrDefault(key: String, ignoreCase:Boolean, defaultValue:V): V = + this.get(key, ignoreCase) ?: defaultValue + + +fun String.toUri():Uri = Uri.parse(this) \ No newline at end of file