mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Further work on http imp.
This commit is contained in:
@@ -51,6 +51,15 @@ class PackageHttpImp : V8Package {
|
|||||||
Logger.w(TAG, "PackageHttpImp Cleaning up")
|
Logger.w(TAG, "PackageHttpImp Cleaning up")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun socket(
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>? = null,
|
||||||
|
useAuth: Boolean = false
|
||||||
|
): Any {
|
||||||
|
throw NotImplementedError("WebSocket is not supported by the curl-impersonate HTTP implementation.")
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T, R> autoParallelPool(
|
private fun <T, R> autoParallelPool(
|
||||||
data: List<T>,
|
data: List<T>,
|
||||||
parallelism: Int,
|
parallelism: Int,
|
||||||
@@ -115,13 +124,30 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useAuth: Boolean = false,
|
useAuth: Boolean = false,
|
||||||
bytesResult: Boolean = false
|
bytesResult: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
return request(method, url, headers, useAuth, bytesResult, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun request(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useAuth: Boolean,
|
||||||
|
bytesResult: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
val client = if (useAuth) _packageClientAuth else _packageClient
|
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||||
|
val parsed = parseRequestOptions(options)
|
||||||
|
val returnType = if (bytesResult) ReturnType.BYTES else ReturnType.STRING
|
||||||
return client.requestInternal(
|
return client.requestInternal(
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
if (bytesResult) ReturnType.BYTES else ReturnType.STRING
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,19 +155,88 @@ class PackageHttpImp : V8Package {
|
|||||||
fun requestWithBody(
|
fun requestWithBody(
|
||||||
method: String,
|
method: String,
|
||||||
url: String,
|
url: String,
|
||||||
body: String,
|
body: Any,
|
||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useAuth: Boolean = false,
|
useAuth: Boolean = false,
|
||||||
bytesResult: Boolean = false
|
bytesResult: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
return requestWithBody(method, url, body, headers, useAuth, bytesResult, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun requestWithBody(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: Any,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useAuth: Boolean,
|
||||||
|
bytesResult: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
val client = if (useAuth) _packageClientAuth else _packageClient
|
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||||
return client.requestWithBodyInternal(
|
val parsed = parseRequestOptions(options)
|
||||||
method,
|
val returnType = if (bytesResult) ReturnType.BYTES else ReturnType.STRING
|
||||||
url,
|
|
||||||
body,
|
return when (body) {
|
||||||
headers,
|
is V8ValueString ->
|
||||||
if (bytesResult) ReturnType.BYTES else ReturnType.STRING
|
client.requestWithBodyInternal(
|
||||||
)
|
method,
|
||||||
|
url,
|
||||||
|
body.value,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
|
is String ->
|
||||||
|
client.requestWithBodyInternal(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
|
is V8ValueTypedArray ->
|
||||||
|
client.requestWithBodyInternal(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body.toBytes(),
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
|
is ByteArray ->
|
||||||
|
client.requestWithBodyInternal(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
|
is ArrayList<*> ->
|
||||||
|
client.requestWithBodyInternal(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
|
else -> throw NotImplementedError(
|
||||||
|
"Body type ${body?.javaClass?.name} not implemented for requestWithBody"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -150,12 +245,27 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useAuth: Boolean = false,
|
useAuth: Boolean = false,
|
||||||
useByteResponse: Boolean = false
|
useByteResponse: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
return GET(url, headers, useAuth, useByteResponse, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun GET(
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useAuth: Boolean,
|
||||||
|
useByteResponse: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
val client = if (useAuth) _packageClientAuth else _packageClient
|
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||||
|
val parsed = parseRequestOptions(options)
|
||||||
return client.GETInternal(
|
return client.GETInternal(
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
if (useByteResponse) ReturnType.BYTES else ReturnType.STRING
|
if (useByteResponse) ReturnType.BYTES else ReturnType.STRING,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,26 +276,77 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useAuth: Boolean = false,
|
useAuth: Boolean = false,
|
||||||
useByteResponse: Boolean = false
|
useByteResponse: Boolean = false
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
return POST(url, body, headers, useAuth, useByteResponse, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun POST(
|
||||||
|
url: String,
|
||||||
|
body: Any,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useAuth: Boolean,
|
||||||
|
useByteResponse: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
val client = if (useAuth) _packageClientAuth else _packageClient
|
val client = if (useAuth) _packageClientAuth else _packageClient
|
||||||
|
val parsed = parseRequestOptions(options)
|
||||||
|
val returnType = if (useByteResponse) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
|
||||||
return when (body) {
|
return when (body) {
|
||||||
is V8ValueString ->
|
is V8ValueString ->
|
||||||
client.POSTInternal(url, body.value, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
client.POSTInternal(
|
||||||
|
url,
|
||||||
|
body.value,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
is String ->
|
is String ->
|
||||||
client.POSTInternal(url, body, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
client.POSTInternal(
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
is V8ValueTypedArray ->
|
is V8ValueTypedArray ->
|
||||||
client.POSTInternal(url, body.toBytes(), headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
client.POSTInternal(
|
||||||
|
url,
|
||||||
|
body.toBytes(),
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
is ByteArray ->
|
is ByteArray ->
|
||||||
client.POSTInternal(url, body, headers, if (useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
client.POSTInternal(
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
|
)
|
||||||
is ArrayList<*> ->
|
is ArrayList<*> ->
|
||||||
client.POSTInternal(
|
client.POSTInternal(
|
||||||
url,
|
url,
|
||||||
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
||||||
headers,
|
headers,
|
||||||
if (useByteResponse) ReturnType.BYTES else ReturnType.STRING
|
returnType,
|
||||||
|
parsed.impersonateTarget,
|
||||||
|
parsed.useBuiltInHeaders,
|
||||||
|
parsed.timeoutMs
|
||||||
)
|
)
|
||||||
else -> throw NotImplementedError("Body type ${body?.javaClass?.name} not implemented for POST")
|
else -> throw NotImplementedError(
|
||||||
|
"Body type ${body?.javaClass?.name} not implemented for POST"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +422,19 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useAuth: Boolean = false
|
useAuth: Boolean = false
|
||||||
): BatchBuilder {
|
): BatchBuilder {
|
||||||
return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers)
|
return request(method, url, headers, useAuth, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun request(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useAuth: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): BatchBuilder {
|
||||||
|
val clientId = _package.getDefaultClient(useAuth).clientId()
|
||||||
|
return clientRequest(clientId, method, url, headers, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -272,7 +445,20 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useAuth: Boolean = false
|
useAuth: Boolean = false
|
||||||
): BatchBuilder {
|
): BatchBuilder {
|
||||||
return clientRequestWithBody(_package.getDefaultClient(useAuth).clientId(), method, url, body, headers)
|
return requestWithBody(method, url, body, headers, useAuth, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun requestWithBody(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useAuth: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): BatchBuilder {
|
||||||
|
val clientId = _package.getDefaultClient(useAuth).clientId()
|
||||||
|
return clientRequestWithBody(clientId, method, url, body, headers, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -281,7 +467,16 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useAuth: Boolean = false
|
useAuth: Boolean = false
|
||||||
): BatchBuilder =
|
): BatchBuilder =
|
||||||
clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers)
|
GET(url, headers, useAuth, null)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun GET(
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useAuth: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): BatchBuilder =
|
||||||
|
clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers, options)
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(
|
fun POST(
|
||||||
@@ -290,7 +485,17 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useAuth: Boolean = false
|
useAuth: Boolean = false
|
||||||
): BatchBuilder =
|
): BatchBuilder =
|
||||||
clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers)
|
POST(url, body, headers, useAuth, null)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun POST(
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useAuth: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): BatchBuilder =
|
||||||
|
clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers, options)
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun DUMMY(): BatchBuilder {
|
fun DUMMY(): BatchBuilder {
|
||||||
@@ -310,7 +515,37 @@ class PackageHttpImp : V8Package {
|
|||||||
url: String,
|
url: String,
|
||||||
headers: MutableMap<String, String> = HashMap()
|
headers: MutableMap<String, String> = HashMap()
|
||||||
): BatchBuilder {
|
): BatchBuilder {
|
||||||
_reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers)))
|
return clientRequest(clientId, method, url, headers, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun clientRequest(
|
||||||
|
clientId: String?,
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): BatchBuilder {
|
||||||
|
val opts = PackageHttpImp.parseRequestOptions(options)
|
||||||
|
val respType =
|
||||||
|
if ((options?.get("useByteResponses") as? Boolean) == true) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
|
||||||
|
_reqs.add(
|
||||||
|
Pair(
|
||||||
|
_package.getClient(clientId),
|
||||||
|
RequestDescriptor(
|
||||||
|
method = method,
|
||||||
|
url = url,
|
||||||
|
headers = headers,
|
||||||
|
body = null,
|
||||||
|
contentType = null,
|
||||||
|
respType = respType,
|
||||||
|
impersonateTarget = opts.impersonateTarget,
|
||||||
|
useBuiltInHeaders = opts.useBuiltInHeaders,
|
||||||
|
timeoutMs = opts.timeoutMs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
return BatchBuilder(_package, _reqs)
|
return BatchBuilder(_package, _reqs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,10 +557,36 @@ class PackageHttpImp : V8Package {
|
|||||||
body: String,
|
body: String,
|
||||||
headers: MutableMap<String, String> = HashMap()
|
headers: MutableMap<String, String> = HashMap()
|
||||||
): BatchBuilder {
|
): BatchBuilder {
|
||||||
|
return clientRequestWithBody(clientId, method, url, body, headers, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun clientRequestWithBody(
|
||||||
|
clientId: String?,
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): BatchBuilder {
|
||||||
|
val opts = PackageHttpImp.parseRequestOptions(options)
|
||||||
|
val respType =
|
||||||
|
if ((options?.get("useByteResponses") as? Boolean) == true) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
|
||||||
_reqs.add(
|
_reqs.add(
|
||||||
Pair(
|
Pair(
|
||||||
_package.getClient(clientId),
|
_package.getClient(clientId),
|
||||||
RequestDescriptor(method, url, headers, body)
|
RequestDescriptor(
|
||||||
|
method = method,
|
||||||
|
url = url,
|
||||||
|
headers = headers,
|
||||||
|
body = body,
|
||||||
|
contentType = null,
|
||||||
|
respType = respType,
|
||||||
|
impersonateTarget = opts.impersonateTarget,
|
||||||
|
useBuiltInHeaders = opts.useBuiltInHeaders,
|
||||||
|
timeoutMs = opts.timeoutMs
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return BatchBuilder(_package, _reqs)
|
return BatchBuilder(_package, _reqs)
|
||||||
@@ -339,6 +600,15 @@ class PackageHttpImp : V8Package {
|
|||||||
): BatchBuilder =
|
): BatchBuilder =
|
||||||
clientRequest(clientId, "GET", url, headers)
|
clientRequest(clientId, "GET", url, headers)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun clientGET(
|
||||||
|
clientId: String?,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): BatchBuilder =
|
||||||
|
clientRequest(clientId, "GET", url, headers, options)
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun clientPOST(
|
fun clientPOST(
|
||||||
clientId: String?,
|
clientId: String?,
|
||||||
@@ -348,26 +618,46 @@ class PackageHttpImp : V8Package {
|
|||||||
): BatchBuilder =
|
): BatchBuilder =
|
||||||
clientRequestWithBody(clientId, "POST", url, body, headers)
|
clientRequestWithBody(clientId, "POST", url, body, headers)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun clientPOST(
|
||||||
|
clientId: String?,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): BatchBuilder =
|
||||||
|
clientRequestWithBody(clientId, "POST", url, body, headers, options)
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun execute(): List<IBridgeHttpResponse?> {
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
return _package.autoParallelPool(_reqs, -1) {
|
return _package.autoParallelPool(_reqs, -1) { pair ->
|
||||||
if (it.second.method == "DUMMY") {
|
val client = pair.first
|
||||||
|
val descriptor = pair.second
|
||||||
|
|
||||||
|
if (descriptor.method == "DUMMY") {
|
||||||
return@autoParallelPool null
|
return@autoParallelPool null
|
||||||
}
|
}
|
||||||
if (it.second.body != null) {
|
|
||||||
it.first.requestWithBodyInternal(
|
if (descriptor.body != null) {
|
||||||
it.second.method,
|
client.requestWithBodyInternal(
|
||||||
it.second.url,
|
descriptor.method,
|
||||||
it.second.body!!,
|
descriptor.url,
|
||||||
it.second.headers,
|
descriptor.body,
|
||||||
it.second.respType
|
descriptor.headers,
|
||||||
|
descriptor.respType,
|
||||||
|
descriptor.impersonateTarget,
|
||||||
|
descriptor.useBuiltInHeaders,
|
||||||
|
descriptor.timeoutMs
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
it.first.requestInternal(
|
client.requestInternal(
|
||||||
it.second.method,
|
descriptor.method,
|
||||||
it.second.url,
|
descriptor.url,
|
||||||
it.second.headers,
|
descriptor.headers,
|
||||||
it.second.respType
|
descriptor.respType,
|
||||||
|
descriptor.impersonateTarget,
|
||||||
|
descriptor.useBuiltInHeaders,
|
||||||
|
descriptor.timeoutMs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.map {
|
}.map {
|
||||||
@@ -401,7 +691,10 @@ class PackageHttpImp : V8Package {
|
|||||||
private var sendCookies: Boolean = true
|
private var sendCookies: Boolean = true
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var persistCookies: Boolean = true
|
private var updateCookies: Boolean = true
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var allowNewCookies: Boolean = true
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var cookieJarPath: String? = null
|
private var cookieJarPath: String? = null
|
||||||
@@ -449,12 +742,12 @@ class PackageHttpImp : V8Package {
|
|||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun setDoUpdateCookies(update: Boolean) {
|
fun setDoUpdateCookies(update: Boolean) {
|
||||||
persistCookies = update
|
updateCookies = update
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun setDoAllowNewCookies(allow: Boolean) {
|
fun setDoAllowNewCookies(allow: Boolean) {
|
||||||
persistCookies = allow
|
allowNewCookies = allow
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -462,6 +755,16 @@ class PackageHttpImp : V8Package {
|
|||||||
this.timeoutMs = timeoutMs
|
this.timeoutMs = timeoutMs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun setDefaultImpersonateTarget(target: String) {
|
||||||
|
impersonateTarget = target
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun setUseBuiltInHeaders(enable: Boolean) {
|
||||||
|
useBuiltInHeaders = enable
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun request(
|
fun request(
|
||||||
method: String,
|
method: String,
|
||||||
@@ -469,21 +772,48 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useBytes: Boolean = false
|
useBytes: Boolean = false
|
||||||
): IBridgeHttpResponse =
|
): IBridgeHttpResponse =
|
||||||
requestInternal(method, url, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
request(method, url, headers, useBytes, null)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun request(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useBytes: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
val opts = PackageHttpImp.parseRequestOptions(options)
|
||||||
|
val returnType = if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
return requestInternal(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
opts.impersonateTarget,
|
||||||
|
opts.useBuiltInHeaders,
|
||||||
|
opts.timeoutMs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun requestInternal(
|
fun requestInternal(
|
||||||
method: String,
|
method: String,
|
||||||
url: String,
|
url: String,
|
||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
returnType: ReturnType
|
returnType: ReturnType,
|
||||||
|
impersonateTargetOverride: String? = null,
|
||||||
|
useBuiltInHeadersOverride: Boolean? = null,
|
||||||
|
timeoutMsOverride: Int? = null
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers)
|
return executeRequest(
|
||||||
return logExceptions {
|
method,
|
||||||
catchHttp {
|
url,
|
||||||
val resp = performCurl(method, url, headers, null)
|
headers,
|
||||||
responseToBridge(resp, returnType)
|
null,
|
||||||
}
|
returnType,
|
||||||
}
|
impersonateTargetOverride,
|
||||||
|
useBuiltInHeadersOverride,
|
||||||
|
timeoutMsOverride
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -494,28 +824,73 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useBytes: Boolean = false
|
useBytes: Boolean = false
|
||||||
): IBridgeHttpResponse =
|
): IBridgeHttpResponse =
|
||||||
requestWithBodyInternal(
|
requestWithBody(method, url, body, headers, useBytes, null)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun requestWithBody(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useBytes: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
val opts = PackageHttpImp.parseRequestOptions(options)
|
||||||
|
val returnType = if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
return requestWithBodyInternal(
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
body,
|
body,
|
||||||
headers,
|
headers,
|
||||||
if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
returnType,
|
||||||
|
opts.impersonateTarget,
|
||||||
|
opts.useBuiltInHeaders,
|
||||||
|
opts.timeoutMs
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun requestWithBodyInternal(
|
fun requestWithBodyInternal(
|
||||||
method: String,
|
method: String,
|
||||||
url: String,
|
url: String,
|
||||||
body: String,
|
body: String,
|
||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
returnType: ReturnType
|
returnType: ReturnType,
|
||||||
|
impersonateTargetOverride: String? = null,
|
||||||
|
useBuiltInHeadersOverride: Boolean? = null,
|
||||||
|
timeoutMsOverride: Int? = null
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers)
|
return executeRequest(
|
||||||
return logExceptions {
|
method,
|
||||||
catchHttp {
|
url,
|
||||||
val resp = performCurl(method, url, headers, body.toByteArray())
|
headers,
|
||||||
responseToBridge(resp, returnType)
|
body.toByteArray(),
|
||||||
}
|
returnType,
|
||||||
}
|
impersonateTargetOverride,
|
||||||
|
useBuiltInHeadersOverride,
|
||||||
|
timeoutMsOverride
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestWithBodyInternal(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
body: ByteArray,
|
||||||
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
|
returnType: ReturnType,
|
||||||
|
impersonateTargetOverride: String? = null,
|
||||||
|
useBuiltInHeadersOverride: Boolean? = null,
|
||||||
|
timeoutMsOverride: Int? = null
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
return executeRequest(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
returnType,
|
||||||
|
impersonateTargetOverride,
|
||||||
|
useBuiltInHeadersOverride,
|
||||||
|
timeoutMsOverride
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -524,20 +899,45 @@ class PackageHttpImp : V8Package {
|
|||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useBytes: Boolean = false
|
useBytes: Boolean = false
|
||||||
): IBridgeHttpResponse =
|
): IBridgeHttpResponse =
|
||||||
GETInternal(url, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
GET(url, headers, useBytes, null)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun GET(
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useBytes: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
val opts = PackageHttpImp.parseRequestOptions(options)
|
||||||
|
val returnType = if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
return GETInternal(
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
opts.impersonateTarget,
|
||||||
|
opts.useBuiltInHeaders,
|
||||||
|
opts.timeoutMs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun GETInternal(
|
fun GETInternal(
|
||||||
url: String,
|
url: String,
|
||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
returnType: ReturnType = ReturnType.STRING
|
returnType: ReturnType = ReturnType.STRING,
|
||||||
|
impersonateTargetOverride: String? = null,
|
||||||
|
useBuiltInHeadersOverride: Boolean? = null,
|
||||||
|
timeoutMsOverride: Int? = null
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers)
|
return executeRequest(
|
||||||
return logExceptions {
|
"GET",
|
||||||
catchHttp {
|
url,
|
||||||
val resp = performCurl("GET", url, headers, null)
|
headers,
|
||||||
responseToBridge(resp, returnType)
|
null,
|
||||||
}
|
returnType,
|
||||||
}
|
impersonateTargetOverride,
|
||||||
|
useBuiltInHeadersOverride,
|
||||||
|
timeoutMsOverride
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -546,22 +946,70 @@ class PackageHttpImp : V8Package {
|
|||||||
body: Any,
|
body: Any,
|
||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
useBytes: Boolean = false
|
useBytes: Boolean = false
|
||||||
|
): IBridgeHttpResponse =
|
||||||
|
POST(url, body, headers, useBytes, null)
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun POST(
|
||||||
|
url: String,
|
||||||
|
body: Any,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
useBytes: Boolean,
|
||||||
|
options: MutableMap<String, Any?>?
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
|
val opts = PackageHttpImp.parseRequestOptions(options)
|
||||||
|
val returnType = if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
||||||
|
|
||||||
return when (body) {
|
return when (body) {
|
||||||
is V8ValueString ->
|
is V8ValueString ->
|
||||||
POSTInternal(url, body.value, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
POSTInternal(
|
||||||
|
url,
|
||||||
|
body.value,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
opts.impersonateTarget,
|
||||||
|
opts.useBuiltInHeaders,
|
||||||
|
opts.timeoutMs
|
||||||
|
)
|
||||||
is String ->
|
is String ->
|
||||||
POSTInternal(url, body, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
POSTInternal(
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
opts.impersonateTarget,
|
||||||
|
opts.useBuiltInHeaders,
|
||||||
|
opts.timeoutMs
|
||||||
|
)
|
||||||
is V8ValueTypedArray ->
|
is V8ValueTypedArray ->
|
||||||
POSTInternal(url, body.toBytes(), headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
POSTInternal(
|
||||||
|
url,
|
||||||
|
body.toBytes(),
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
opts.impersonateTarget,
|
||||||
|
opts.useBuiltInHeaders,
|
||||||
|
opts.timeoutMs
|
||||||
|
)
|
||||||
is ByteArray ->
|
is ByteArray ->
|
||||||
POSTInternal(url, body, headers, if (useBytes) ReturnType.BYTES else ReturnType.STRING)
|
POSTInternal(
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
returnType,
|
||||||
|
opts.impersonateTarget,
|
||||||
|
opts.useBuiltInHeaders,
|
||||||
|
opts.timeoutMs
|
||||||
|
)
|
||||||
is ArrayList<*> ->
|
is ArrayList<*> ->
|
||||||
POSTInternal(
|
POSTInternal(
|
||||||
url,
|
url,
|
||||||
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
body.map { (it as Double).toInt().toByte() }.toByteArray(),
|
||||||
headers,
|
headers,
|
||||||
if (useBytes) ReturnType.BYTES else ReturnType.STRING
|
returnType,
|
||||||
|
opts.impersonateTarget,
|
||||||
|
opts.useBuiltInHeaders,
|
||||||
|
opts.timeoutMs
|
||||||
)
|
)
|
||||||
else -> throw NotImplementedError("Body type ${body?.javaClass?.name} not implemented for POST")
|
else -> throw NotImplementedError("Body type ${body?.javaClass?.name} not implemented for POST")
|
||||||
}
|
}
|
||||||
@@ -571,55 +1019,101 @@ class PackageHttpImp : V8Package {
|
|||||||
url: String,
|
url: String,
|
||||||
body: String,
|
body: String,
|
||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
returnType: ReturnType = ReturnType.STRING
|
returnType: ReturnType = ReturnType.STRING,
|
||||||
|
impersonateTargetOverride: String? = null,
|
||||||
|
useBuiltInHeadersOverride: Boolean? = null,
|
||||||
|
timeoutMsOverride: Int? = null
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers)
|
return executeRequest(
|
||||||
return logExceptions {
|
"POST",
|
||||||
catchHttp {
|
url,
|
||||||
val resp = performCurl("POST", url, headers, body.toByteArray())
|
headers,
|
||||||
responseToBridge(resp, returnType)
|
body.toByteArray(),
|
||||||
}
|
returnType,
|
||||||
}
|
impersonateTargetOverride,
|
||||||
|
useBuiltInHeadersOverride,
|
||||||
|
timeoutMsOverride
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun POSTInternal(
|
fun POSTInternal(
|
||||||
url: String,
|
url: String,
|
||||||
body: ByteArray,
|
body: ByteArray,
|
||||||
headers: MutableMap<String, String> = HashMap(),
|
headers: MutableMap<String, String> = HashMap(),
|
||||||
returnType: ReturnType = ReturnType.STRING
|
returnType: ReturnType = ReturnType.STRING,
|
||||||
|
impersonateTargetOverride: String? = null,
|
||||||
|
useBuiltInHeadersOverride: Boolean? = null,
|
||||||
|
timeoutMsOverride: Int? = null
|
||||||
): IBridgeHttpResponse {
|
): IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers)
|
return executeRequest(
|
||||||
return logExceptions {
|
"POST",
|
||||||
catchHttp {
|
url,
|
||||||
val resp = performCurl("POST", url, headers, body)
|
headers,
|
||||||
responseToBridge(resp, returnType)
|
body,
|
||||||
}
|
returnType,
|
||||||
}
|
impersonateTargetOverride,
|
||||||
|
useBuiltInHeadersOverride,
|
||||||
|
timeoutMsOverride
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performCurl(
|
private fun performCurl(
|
||||||
method: String,
|
method: String,
|
||||||
url: String,
|
url: String,
|
||||||
headers: Map<String, String>,
|
headers: Map<String, String>,
|
||||||
bodyBytes: ByteArray?
|
bodyBytes: ByteArray?,
|
||||||
|
impersonateTargetOverride: String? = null,
|
||||||
|
useBuiltInHeadersOverride: Boolean? = null,
|
||||||
|
timeoutMsOverride: Int? = null
|
||||||
): Libcurl.Response {
|
): Libcurl.Response {
|
||||||
val jar = ensureCookieJarPath()
|
val jar = ensureCookieJarPath()
|
||||||
|
|
||||||
|
val finalImpersonateTarget = impersonateTargetOverride ?: this.impersonateTarget
|
||||||
|
val finalUseBuiltInHeaders = useBuiltInHeadersOverride ?: this.useBuiltInHeaders
|
||||||
|
val finalTimeoutMs = timeoutMsOverride ?: this.timeoutMs
|
||||||
|
|
||||||
val req = Libcurl.Request(
|
val req = Libcurl.Request(
|
||||||
url = url,
|
url = url,
|
||||||
method = method,
|
method = method,
|
||||||
headers = headers,
|
headers = headers,
|
||||||
body = bodyBytes,
|
body = bodyBytes,
|
||||||
impersonateTarget = impersonateTarget,
|
impersonateTarget = finalImpersonateTarget,
|
||||||
useBuiltInHeaders = useBuiltInHeaders,
|
useBuiltInHeaders = finalUseBuiltInHeaders,
|
||||||
timeoutMs = timeoutMs,
|
timeoutMs = finalTimeoutMs,
|
||||||
cookieJarPath = jar,
|
cookieJarPath = jar,
|
||||||
sendCookies = sendCookies,
|
sendCookies = sendCookies,
|
||||||
persistCookies = persistCookies
|
persistCookies = updateCookies && allowNewCookies
|
||||||
)
|
)
|
||||||
return Libcurl.perform(req)
|
return Libcurl.perform(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun executeRequest(
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: MutableMap<String, String>,
|
||||||
|
bodyBytes: ByteArray?,
|
||||||
|
returnType: ReturnType,
|
||||||
|
impersonateTargetOverride: String? = null,
|
||||||
|
useBuiltInHeadersOverride: Boolean? = null,
|
||||||
|
timeoutMsOverride: Int? = null
|
||||||
|
): IBridgeHttpResponse {
|
||||||
|
applyDefaultHeaders(headers)
|
||||||
|
return logExceptions {
|
||||||
|
catchHttp {
|
||||||
|
val resp = performCurl(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
bodyBytes,
|
||||||
|
impersonateTargetOverride,
|
||||||
|
useBuiltInHeadersOverride,
|
||||||
|
timeoutMsOverride
|
||||||
|
)
|
||||||
|
responseToBridge(resp, returnType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun responseToBridge(
|
private fun responseToBridge(
|
||||||
resp: Libcurl.Response,
|
resp: Libcurl.Response,
|
||||||
returnType: ReturnType
|
returnType: ReturnType
|
||||||
@@ -754,7 +1248,10 @@ class PackageHttpImp : V8Package {
|
|||||||
val headers: MutableMap<String, String>,
|
val headers: MutableMap<String, String>,
|
||||||
val body: String? = null,
|
val body: String? = null,
|
||||||
val contentType: String? = null,
|
val contentType: String? = null,
|
||||||
val respType: ReturnType = ReturnType.STRING
|
val respType: ReturnType = ReturnType.STRING,
|
||||||
|
val impersonateTarget: String? = null,
|
||||||
|
val useBuiltInHeaders: Boolean? = null,
|
||||||
|
val timeoutMs: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun catchHttp(handle: () -> BridgeHttpStringResponse): BridgeHttpStringResponse {
|
private fun catchHttp(handle: () -> BridgeHttpStringResponse): BridgeHttpStringResponse {
|
||||||
@@ -783,5 +1280,47 @@ class PackageHttpImp : V8Package {
|
|||||||
"content-disposition",
|
"content-disposition",
|
||||||
"connection"
|
"connection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal data class RequestOptions(
|
||||||
|
val impersonateTarget: String? = null,
|
||||||
|
val useBuiltInHeaders: Boolean? = null,
|
||||||
|
val timeoutMs: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun parseRequestOptions(options: Map<String, Any?>?): RequestOptions {
|
||||||
|
if (options == null) return RequestOptions()
|
||||||
|
|
||||||
|
var impersonateTarget: String? = null
|
||||||
|
var useBuiltInHeaders: Boolean? = null
|
||||||
|
var timeoutMs: Int? = null
|
||||||
|
|
||||||
|
options["impersonateTarget"]?.let { v ->
|
||||||
|
val s = v as? String
|
||||||
|
if (!s.isNullOrBlank()) {
|
||||||
|
impersonateTarget = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options["useBuiltInHeaders"]?.let { v ->
|
||||||
|
if (v is Boolean) {
|
||||||
|
useBuiltInHeaders = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options["timeoutMs"]?.let { v ->
|
||||||
|
val n: Long? = when (v) {
|
||||||
|
is Int -> v.toLong()
|
||||||
|
is Long -> v
|
||||||
|
is Double -> v.toLong()
|
||||||
|
is Float -> v.toLong()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (n != null && n > 0 && n <= Int.MAX_VALUE) {
|
||||||
|
timeoutMs = n.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RequestOptions(impersonateTarget, useBuiltInHeaders, timeoutMs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user