package com.example.service import android.net.Uri import android.util.Log import com.example.action.BaseAction import com.example.action.HttpActionRequest import com.example.action.HttpMethod import com.example.action.NameValue import com.example.action.NameVariable import com.example.action.Next import com.example.http.HttpClient import com.example.http.HttpClient.call import com.example.http.Request import com.example.http.Response import com.example.logger.LogUtils import com.example.report.ActionExec import com.example.task.TaskConfig import com.example.utils.toJsonString import com.example.utils.toJsonString1 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.net.URI import java.net.URL import kotlin.time.DurationUnit import kotlin.time.toDuration class HttpService( private val action: BaseAction.HttpAction, override val taskConfig: TaskConfig ) : BaseService(taskConfig) { companion object { private const val MAX_REDIRECT_COUNT = 30 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() val currentStep = taskConfig.currentStep try { handleActionDelay() val actionRequest = action.request ?: throw NullPointerException("request is null") amendActionRequest(actionRequest) val httpRequest = actionRequest.buildHttpRequest() val result = if (action.async) { handleAsyncRequest(httpRequest, actionExecList) } else { handleSyncRequest(httpRequest, actionExecList) } updateTaskStep(result, currentStep, httpRequest) } catch (e: Exception) { LogUtils.error(throwable = e) 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 ): ActionExec { val actionExec = ActionExec( step = taskConfig.currentStep, index = redirectCount, time = System.currentTimeMillis(), url = url, method = method?.value ?: "GET", reqHeader = headers.toJsonString1() ) if (body.isNotEmpty()) { actionExec.reqData = String(body) } httpResponse?.let { response -> saveCookiesFromResponse(response, url) fillResponseData(actionExec, response) } return actionExec } 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) } }.onFailure { LogUtils.error(throwable = it) } } private fun fillResponseData(actionExec: ActionExec, response: Response) { if (response.headers.isNotEmpty()) { actionExec.respHeader = response.headers.toJsonString() } 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 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 } } } // ==================== 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, method = method, body = data.toByteArray(), headers = headers.nameValueToMap() ) private fun genWholeResponse(httpRequest: Request, httpResponse: Response?): String { 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 ): Int { val wholeResponse = genWholeResponse(httpRequest, httpResponse) return getNextStepIndex(wholeResponse, currentStep) } // ==================== 变量提取 ==================== private fun extractResponseVariableToCache( action: BaseAction.HttpAction, httpRequest: Request, httpResponse: Response ) { 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> ) { if (cookies.isEmpty()) return runCatching { 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> ) { if (headers.isEmpty() || responseHeaders.isEmpty()) return 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() 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() } // 添加 Sec-CH-UA Header (HTTPS) if (actionRequest.url.isHttps()) { actionRequest.headers = actionRequest.headers.addSecChUa() } // 处理 Cookie actionRequest.amendCookie() // 处理 POST 数据 if (actionRequest.method != HttpMethod.Get) { val sendData = actionRequest.genPostData() if (sendData.isNotEmpty()) { actionRequest.data = String(sendData) } } } private fun buildGetUrl(actionRequest: HttpActionRequest): String { 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 = mutableSetOf() // 从 CookieManager 获取 Cookie if (autoCookie) { cookies += cookieFromCookieManager(url) } // 添加手动指定的 Cookie(覆盖同名 Cookie) this.cookies.forEach { cookies.remove(it) cookies += it } // 构建 Cookie Header if (cookies.isNotEmpty()) { headers += cookies.toList().buildCookie() } } private fun HttpActionRequest.genPostData(): ByteArray { // 优先使用 params(form-data) if (params.isNotEmpty()) { return params.joinToString("&") { "${it.name.urlEncode()}=${it.value.urlEncode()}" }.toByteArray() } // 否则使用 data(raw data) if (data.isNotBlank()) { return data.toByteArray(Charsets.UTF_8) } return byteArrayOf() } // ==================== 重定向请求构建 ==================== 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[cookie.name] = cookie.value } } // 添加 Sec-CH-UA Header (HTTPS) if (normalizedUrl.isHttps()) { mutableListOf().addSecChUa().forEach { headers[it.name] = it.value } } return Request( url = normalizedUrl, method = HttpMethod.Get, headers = headers ) } } // ==================== Map 扩展函数 ==================== fun Map.get(key: String, ignoreCase: Boolean): V? { 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 fun String.toUri():Uri = Uri.parse(this)