You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

555 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<ActionExec>) -> Unit) =
withContext(Dispatchers.Default) {
val actionExecList = mutableListOf<ActionExec>()
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<ActionExec>
): 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<ActionExec>
): 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<ActionExec>
): 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<ActionExec>,
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<Next>.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<NameVariable>,
responseHeaders: Map<String, List<String>>
) {
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<NameVariable>,
responseHeaders: Map<String, List<String>>
) {
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<NameValue>()
// 从 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 {
// 优先使用 paramsform-data
if (params.isNotEmpty()) {
return params.joinToString("&") {
"${it.name.urlEncode()}=${it.value.urlEncode()}"
}.toByteArray()
}
// 否则使用 dataraw 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<NameValue>().addSecChUa().forEach {
headers[it.name] = it.value
}
}
return Request(
url = normalizedUrl,
method = HttpMethod.Get,
headers = headers
)
}
}
// ==================== Map 扩展函数 ====================
fun <V> Map<String, V>.get(key: String, ignoreCase: Boolean): V? {
return entries.firstOrNull {
it.key.equals(key, ignoreCase = ignoreCase)
}?.value
}
fun <V> Map<String, V>.getOrDefault(key: String, ignoreCase:Boolean, defaultValue:V): V =
this.get(key, ignoreCase) ?: defaultValue
fun String.toUri():Uri = Uri.parse(this)