|
|
|
|
@ -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()
|
|
|
|
|
// 优先使用 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<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)
|