mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 14:32:28 +00:00
fix: bound generic Android raw responses
This commit is contained in:
parent
250d9d79cf
commit
50bb2bcc52
4 changed files with 58 additions and 1 deletions
|
|
@ -16,6 +16,8 @@ import okhttp3.OkHttpClient
|
|||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.net.Proxy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import kotlin.text.Charsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
|
@ -87,6 +89,11 @@ private val addonHttpClient = OkHttpClient.Builder()
|
|||
|
||||
private val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
private data class LimitedReadResult(
|
||||
val bytes: ByteArray,
|
||||
val truncated: Boolean,
|
||||
)
|
||||
|
||||
private fun requestAllowsBody(method: String): Boolean =
|
||||
when (method.uppercase()) {
|
||||
"POST", "PUT", "PATCH", "DELETE" -> true
|
||||
|
|
@ -101,6 +108,42 @@ private fun Map<String, String>.withoutAcceptEncoding(): Map<String, String> =
|
|||
private fun Map<String, String>.getHeaderIgnoreCase(name: String): String? =
|
||||
entries.firstOrNull { (key, _) -> key.equals(name, ignoreCase = true) }?.value
|
||||
|
||||
private fun readAtMostBytes(stream: InputStream, maxBytes: Int): LimitedReadResult {
|
||||
val out = ByteArrayOutputStream(minOf(maxBytes, 16 * 1024))
|
||||
val buffer = ByteArray(8 * 1024)
|
||||
var remaining = maxBytes
|
||||
var truncated = false
|
||||
|
||||
while (remaining > 0) {
|
||||
val read = stream.read(buffer, 0, minOf(buffer.size, remaining))
|
||||
if (read <= 0) break
|
||||
out.write(buffer, 0, read)
|
||||
remaining -= read
|
||||
}
|
||||
|
||||
if (remaining == 0) {
|
||||
truncated = stream.read() != -1
|
||||
}
|
||||
|
||||
return LimitedReadResult(out.toByteArray(), truncated)
|
||||
}
|
||||
|
||||
private fun readResponseBodyLimited(body: ResponseBody?, maxBytes: Int): String {
|
||||
if (body == null) return ""
|
||||
val charset = body.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8
|
||||
val readResult = body.byteStream().use { stream ->
|
||||
readAtMostBytes(stream, maxBytes.coerceAtLeast(0))
|
||||
}
|
||||
|
||||
val decoded = try {
|
||||
String(readResult.bytes, charset)
|
||||
} catch (_: Exception) {
|
||||
String(readResult.bytes, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
return if (readResult.truncated) "$decoded\n...[truncated]" else decoded
|
||||
}
|
||||
|
||||
private fun readResponseBody(body: ResponseBody?): String {
|
||||
if (body == null) return ""
|
||||
val bytes = body.bytes()
|
||||
|
|
@ -196,6 +239,7 @@ actual suspend fun httpRequestRaw(
|
|||
headers: Map<String, String>,
|
||||
body: String,
|
||||
followRedirects: Boolean,
|
||||
maxResponseBodyBytes: Int,
|
||||
): RawHttpResponse =
|
||||
withContext(Dispatchers.IO) {
|
||||
val normalizedMethod = method.uppercase()
|
||||
|
|
@ -228,7 +272,7 @@ actual suspend fun httpRequestRaw(
|
|||
status = response.code,
|
||||
statusText = response.message,
|
||||
url = response.request.url.toString(),
|
||||
body = readResponseBody(response.body),
|
||||
body = readResponseBodyLimited(response.body, maxResponseBodyBytes),
|
||||
headers = response.headers.toMultimap().mapValues { (_, values) ->
|
||||
values.joinToString(",")
|
||||
}.mapKeys { (name, _) ->
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ data class RawHttpResponse(
|
|||
val headers: Map<String, String>,
|
||||
)
|
||||
|
||||
/** Default safety limit for generic and plugin-provided HTTP responses. */
|
||||
internal const val DefaultRawHttpResponseMaxBytes = 1024 * 1024
|
||||
|
||||
expect suspend fun httpGetText(url: String): String
|
||||
|
||||
expect suspend fun httpPostJson(url: String, body: String): String
|
||||
|
|
@ -36,4 +39,5 @@ expect suspend fun httpRequestRaw(
|
|||
headers: Map<String, String>,
|
||||
body: String,
|
||||
followRedirects: Boolean = true,
|
||||
maxResponseBodyBytes: Int = DefaultRawHttpResponseMaxBytes,
|
||||
): RawHttpResponse
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.nuvio.app.features.debrid
|
||||
|
||||
import com.nuvio.app.features.addons.RawHttpResponse
|
||||
import com.nuvio.app.features.addons.DefaultRawHttpResponseMaxBytes
|
||||
import com.nuvio.app.features.addons.httpRequestRaw
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerializationException
|
||||
|
|
@ -27,6 +28,8 @@ internal object DebridApiJson {
|
|||
|
||||
internal object TorboxApiClient {
|
||||
private const val BASE_URL = "https://api.torbox.app"
|
||||
// Torbox returns up to 1,000 items, each with its full files array.
|
||||
private const val cloudListResponseMaxBytes = 16 * 1024 * 1024
|
||||
|
||||
suspend fun startDeviceAuthorization(
|
||||
appName: String,
|
||||
|
|
@ -120,6 +123,7 @@ internal object TorboxApiClient {
|
|||
method = "GET",
|
||||
url = "$BASE_URL/v1/api/torrents/mylist",
|
||||
apiKey = apiKey,
|
||||
maxResponseBodyBytes = cloudListResponseMaxBytes,
|
||||
)
|
||||
|
||||
suspend fun listCloudUsenet(apiKey: String): DebridApiResponse<TorboxEnvelopeDto<List<TorboxCloudItemDto>>> =
|
||||
|
|
@ -127,6 +131,7 @@ internal object TorboxApiClient {
|
|||
method = "GET",
|
||||
url = "$BASE_URL/v1/api/usenet/mylist",
|
||||
apiKey = apiKey,
|
||||
maxResponseBodyBytes = cloudListResponseMaxBytes,
|
||||
)
|
||||
|
||||
suspend fun listCloudWebDownloads(apiKey: String): DebridApiResponse<TorboxEnvelopeDto<List<TorboxCloudItemDto>>> =
|
||||
|
|
@ -134,6 +139,7 @@ internal object TorboxApiClient {
|
|||
method = "GET",
|
||||
url = "$BASE_URL/v1/api/webdl/mylist",
|
||||
apiKey = apiKey,
|
||||
maxResponseBodyBytes = cloudListResponseMaxBytes,
|
||||
)
|
||||
|
||||
suspend fun requestDownloadLink(
|
||||
|
|
@ -222,6 +228,7 @@ internal object TorboxApiClient {
|
|||
apiKey: String,
|
||||
body: String = "",
|
||||
contentType: String? = null,
|
||||
maxResponseBodyBytes: Int = DefaultRawHttpResponseMaxBytes,
|
||||
): DebridApiResponse<T> {
|
||||
val headers = authHeaders(apiKey) + listOfNotNull(
|
||||
contentType?.let { "Content-Type" to it },
|
||||
|
|
@ -232,6 +239,7 @@ internal object TorboxApiClient {
|
|||
url = url,
|
||||
headers = headers,
|
||||
body = body,
|
||||
maxResponseBodyBytes = maxResponseBodyBytes,
|
||||
)
|
||||
return DebridApiResponse(
|
||||
status = response.status,
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ actual suspend fun httpRequestRaw(
|
|||
headers: Map<String, String>,
|
||||
body: String,
|
||||
followRedirects: Boolean,
|
||||
maxResponseBodyBytes: Int,
|
||||
): RawHttpResponse =
|
||||
addonHttpClient
|
||||
.request {
|
||||
|
|
|
|||
Loading…
Reference in a new issue