parent
63ab06eefa
commit
efe559c6d2
@ -1,4 +1,439 @@
|
|||||||
package com.example.service
|
package com.example.service
|
||||||
|
|
||||||
class HttpService {
|
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.isActive
|
||||||
|
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
|
||||||
|
val pattern = Regex("^[a-zA-Z0-9]+://.*")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun execute(onFinish: (List<ActionExec>) -> Unit) =
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val actionExecList = mutableListOf<ActionExec>()
|
||||||
|
var 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))
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
taskConfig.currentStep = Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.info("finish action: ${action.request?.url}")
|
||||||
|
onFinish(actionExecList)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
actionExec.respCode = response.code
|
||||||
|
if (response.data.isNotEmpty()) {
|
||||||
|
actionExec.respData = String(response.data)
|
||||||
|
}
|
||||||
|
actionExec.cost = httpResponse.endTime - httpResponse.startTime
|
||||||
|
|
||||||
|
}
|
||||||
|
return actionExec
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.makeAsyncRequest() = scope.launch {
|
||||||
|
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()
|
||||||
|
}.onFailure {
|
||||||
|
LogUtils.error(throwable = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.isHttpRedirect(): Boolean = pattern.find(this)?.let {
|
||||||
|
return@isHttpRedirect this.startsWith("http")
|
||||||
|
} ?: 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 {
|
||||||
|
return """
|
||||||
|
[${httpRequest.url}]${if (httpResponse?.data?.isNotEmpty() == true) String(httpResponse.data) else ""}[${
|
||||||
|
httpResponse?.headers?.let {
|
||||||
|
if (it.isEmpty()) ""
|
||||||
|
else it.toJsonString()
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
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?
|
||||||
|
) {
|
||||||
|
action.response?.let { actionResponse ->
|
||||||
|
httpResponse?.headers?.let { responseHeaders ->
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}.onFailure {
|
||||||
|
LogUtils.error(throwable = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractHeaderVariableToCache(
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
actionRequest.url = when (actionRequest.method) {
|
||||||
|
HttpMethod.Get -> {
|
||||||
|
buildGetUrl(actionRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
actionRequest.url.toVariableData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionRequest.url.startsWith("https", ignoreCase = true)) {
|
||||||
|
actionRequest.headers.addSecChUa().apply {
|
||||||
|
actionRequest.headers = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actionRequest.amendCookie()
|
||||||
|
if (HttpMethod.Get != actionRequest.method) {
|
||||||
|
val sendData: ByteArray = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HttpActionRequest.amendCookie() {
|
||||||
|
val cookies: MutableSet<NameValue> = mutableSetOf()
|
||||||
|
cookieFromCookieManager(url).apply {
|
||||||
|
if (autoCookie && isNotEmpty()) {
|
||||||
|
cookies += this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cookies.apply {
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
forEach {
|
||||||
|
cookies.remove(it)
|
||||||
|
cookies += it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cookies.isNotEmpty()) {
|
||||||
|
headers += cookies.toList().buildCookie()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HttpActionRequest.genPostData(): ByteArray {
|
||||||
|
var sendData: ByteArray = byteArrayOf()
|
||||||
|
if (params.isNotEmpty()) {
|
||||||
|
sendData = params.joinToString("&") {
|
||||||
|
"${it.name.urlEncode()}=${it.value.urlEncode()}"
|
||||||
|
}.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.isNotBlank()) {
|
||||||
|
sendData = data.toByteArray(Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
return sendData
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (cookies.isNotEmpty()) {
|
||||||
|
cookies.toList().buildCookie().let { cookie ->
|
||||||
|
headers.put(cookie.name, cookie.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.url.isHttps()) {
|
||||||
|
val secChUa = mutableListOf<NameValue>().addSecChUa()
|
||||||
|
secChUa.forEach {
|
||||||
|
headers[it.name] = it.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <V> Map<String, V>.get(key: String, ignoreCase: Boolean): V? {
|
||||||
|
return this.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
|
||||||
Loading…
Reference in new issue