refactor: 优化 HttpService 代码,修复类型问题和改进代码质量

main
mojo 1 month ago
parent efe559c6d2
commit fbc899a824

@ -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<ActionExec>) -> Unit) =
withContext(Dispatchers.Default) {
val actionExecList = mutableListOf<ActionExec>()
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<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
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<String>
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<Next>.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<NameVariable>, responseHeaders: Map<String, List<String>>
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)
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<NameVariable>, responseHeaders: Map<String, List<String>>
headers: List<NameVariable>,
responseHeaders: Map<String, List<String>>
) {
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<NameValue> = mutableSetOf()
cookieFromCookieManager(url).apply {
if (autoCookie && isNotEmpty()) {
cookies += this
}
val cookies = mutableSetOf<NameValue>()
// 从 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()
// 优先使用 paramsform-data
if (params.isNotEmpty()) {
sendData = params.joinToString("&") {
return params.joinToString("&") {
"${it.name.urlEncode()}=${it.value.urlEncode()}"
}.toByteArray()
}
// 否则使用 dataraw 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<NameValue>().addSecChUa()
secChUa.forEach {
// 添加 Sec-CH-UA Header (HTTPS)
if (normalizedUrl.isHttps()) {
mutableListOf<NameValue>().addSecChUa().forEach {
headers[it.name] = it.value
}
}
return request
return Request(
url = normalizedUrl,
method = HttpMethod.Get,
headers = headers
)
}
}
// ==================== Map 扩展函数 ====================
fun <V> Map<String, V>.get(key: String, ignoreCase: Boolean): V? {
return this.entries.firstOrNull {
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 <V> Map<String, V>.getOrDefault(key: String, ignoreCase:Boolean, defaultValue:V): V =
this.get(key, ignoreCase) ?: defaultValue
fun String.toUri():Uri = Uri.parse(this)
Loading…
Cancel
Save