SoundCloud Parsing Fixes and HttpClient SSL Certificates Validation Ignore

This commit is contained in:
Shabinder 2022-01-27 00:47:57 +05:30
parent d5838db298
commit 9b3b00f0d2
14 changed files with 171 additions and 50 deletions

View File

@ -55,6 +55,7 @@ android {
targetSdk = Versions.targetSdkVersion
versionCode = Versions.versionCode
versionName = Versions.versionName
ndkVersion = "21.4.7075529"
}
buildTypes {
getByName("release") {

View File

@ -26,9 +26,9 @@ import org.gradle.kotlin.dsl.getByType
object Versions {
// App's Version (To be bumped at each update)
const val versionName = "3.5.0"
const val versionName = "3.6.0"
const val versionCode = 27
const val versionCode = 28
// Android
const val minSdkVersion = 21
@ -78,6 +78,7 @@ val VersionCatalog.kotlinCoroutines get() = findDependency("kotlin-coroutines").
val VersionCatalog.kotlinxSerialization get() = findDependency("kotlinx-serialization-json").get()
val VersionCatalog.ktorClientIOS get() = findDependency("ktor-client-ios").get()
val VersionCatalog.ktorClientAndroid get() = findDependency("ktor-client-android").get()
val VersionCatalog.ktorClientAndroidOkHttp get() = findDependency("ktor-client-okhttp").get()
val VersionCatalog.ktorClientApache get() = findDependency("ktor-client-apache").get()
val VersionCatalog.ktorClientJS get() = findDependency("ktor-client-js").get()
val VersionCatalog.ktorClientCIO get() = findDependency("ktor-client-cio").get()

View File

@ -58,6 +58,7 @@ ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version
ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serialization", version.ref = "ktor" }
ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" }
ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" }
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-client-curl = { group = "io.ktor", name = "ktor-client-curl", version.ref = "ktor" }
ktor-client-apache = { group = "io.ktor", name = "ktor-client-apache", version.ref = "ktor" }
ktor-client-ios = { group = "io.ktor", name = "ktor-client-ios", version.ref = "ktor" }

View File

@ -67,7 +67,7 @@ kotlin {
implementation(compose.materialIconsExtended)
implementation(Deps.androidXCommonBundle)
implementation(Deps.decomposeComposeExt)
implementation(Deps.ktorClientAndroid)
implementation(Deps.ktorClientAndroidOkHttp)
implementation(Deps.koinAndroidBundle)
}
}

View File

@ -210,7 +210,7 @@ class AndroidFileManager(
// Get Memory Efficient Bitmap
val bitmap: Bitmap? = getMemoryEfficientBitmap(input, reqWidth, reqHeight)
parallelExecutor.executeSuspending {
parallelExecutor.execute {
// Decode and Cache Full Sized Image in Background
cacheImage(
BitmapFactory.decodeByteArray(input, 0, input.size),

View File

@ -40,7 +40,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
title = track.title
album = track.albumName
year = track.year
comment = "Genres:${track.comment}"
comment = "${track.comment}"
if (track.trackNumber != null)
this.track = track.trackNumber.toString()
}

View File

@ -0,0 +1,55 @@
package com.shabinder.common.core_components.utils
import android.annotation.SuppressLint
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.engine.okhttp.OkHttpConfig
import okhttp3.OkHttpClient
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
actual fun buildHttpClient(extraConfig: HttpClientConfig<*>.() -> Unit): HttpClient {
return HttpClient(OkHttp) {
engine {
preconfigured = getUnsafeOkHttpClient()
}
extraConfig()
}
}
fun getUnsafeOkHttpClient(): OkHttpClient {
return try {
// Create a trust manager that does not validate certificate chains
@SuppressLint("CustomX509TrustManager")
val trustAllCerts: TrustManager = object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<X509Certificate?>?, authType: String?) {
}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<X509Certificate?>?, authType: String?) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
// Install the all-trusting trust manager
val sslContext: SSLContext = SSLContext.getInstance("SSL").apply {
init(null, arrayOf(trustAllCerts), SecureRandom())
}
OkHttpClient.Builder().run {
sslSocketFactory(sslContext.socketFactory, trustAllCerts as X509TrustManager)
hostnameVerifier { _, _ -> true }
followRedirects(true)
build()
}
} catch (e: Exception) {
throw RuntimeException(e)
}
}

View File

@ -40,15 +40,6 @@ interface ParallelProcessor {
}
}
suspend fun <T> executeSafelyInPool(
onComplete: suspend (result: SuspendableEvent<T, Throwable>) -> Unit = {},
block: suspend () -> T
): SuspendableEvent<T, Throwable> {
return SuspendableEvent {
parallelExecutor.executeSuspending(block)
}.also { onComplete(it) }
}
suspend fun stopAllTasks() {
parallelExecutor.closeAndReInit()
}

View File

@ -2,14 +2,20 @@ package com.shabinder.common.core_components.utils
import com.shabinder.common.models.dispatcherIO
import com.shabinder.common.utils.globalJson
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
import io.ktor.client.request.*
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.features.HttpTimeout
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.DEFAULT
import io.ktor.client.features.logging.LogLevel
import io.ktor.client.features.logging.Logger
import io.ktor.client.features.logging.Logging
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.request.head
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers
import io.ktor.http.ContentType
import kotlinx.coroutines.withContext
import kotlin.native.concurrent.SharedImmutable
@ -32,39 +38,42 @@ suspend inline fun HttpClient.getFinalUrl(
): String {
return withContext(dispatcherIO) {
runCatching {
get<HttpResponse>(url,block).call.request.url.toString()
get<HttpResponse>(url, block).call.request.url.toString()
}.getOrNull() ?: url
}
}
fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient {
// https://github.com/Kotlin/kotlinx.serialization/issues/1450
install(JsonFeature) {
serializer = KotlinxSerializer(globalJson)
}
install(HttpTimeout) {
socketTimeoutMillis = 520_000
requestTimeoutMillis = 360_000
connectTimeoutMillis = 360_000
}
// WorkAround for Freezing
// Use httpClient.getData / httpClient.postData Extensions
/*install(JsonFeature) {
serializer = KotlinxSerializer(
Json {
isLenient = true
ignoreUnknownKeys = true
buildHttpClient {
// https://github.com/Kotlin/kotlinx.serialization/issues/1450
install(JsonFeature) {
serializer = KotlinxSerializer(globalJson)
}
install(HttpTimeout) {
socketTimeoutMillis = 520_000
requestTimeoutMillis = 360_000
connectTimeoutMillis = 360_000
}
// WorkAround for Freezing
// Use httpClient.getData / httpClient.postData Extensions
/*install(JsonFeature) {
serializer = KotlinxSerializer(
Json {
isLenient = true
ignoreUnknownKeys = true
}
)
}*/
if (enableNetworkLogs) {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.INFO
}
)
}*/
if (enableNetworkLogs) {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.INFO
}
}
}
expect fun buildHttpClient(extraConfig: HttpClientConfig<*>.() -> Unit): HttpClient
/*Client Active Throughout App's Lifetime*/
@SharedImmutable

View File

@ -0,0 +1,25 @@
package com.shabinder.common.core_components.utils
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.apache.Apache
import org.apache.http.conn.ssl.NoopHostnameVerifier
import org.apache.http.conn.ssl.TrustSelfSignedStrategy
import org.apache.http.ssl.SSLContextBuilder
actual fun buildHttpClient(extraConfig: HttpClientConfig<*>.() -> Unit): HttpClient {
return HttpClient(Apache) {
engine {
customizeClient {
setSSLContext(
SSLContextBuilder
.create()
.loadTrustMaterial(TrustSelfSignedStrategy())
.build()
)
setSSLHostnameVerifier(NoopHostnameVerifier())
}
}
extraConfig()
}
}

View File

@ -0,0 +1,9 @@
package com.shabinder.common.core_components.utils
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.js.Js
actual fun buildHttpClient(extraConfig: HttpClientConfig<*>.() -> Unit): HttpClient = HttpClient(Js) {
extraConfig()
}

View File

@ -31,7 +31,7 @@ sealed class SoundCloudResolveResponseBase {
@SerialName("embeddable_by")
val embeddableBy: String = "",
val genre: String = "",
val id: Int = 0,
val id: String = "",
@SerialName("is_album")
val isAlbum: Boolean = false,
@SerialName("label_name")
@ -100,7 +100,7 @@ sealed class SoundCloudResolveResponseBase {
val genre: String = "",
@SerialName("has_downloads_left")
val hasDownloadsLeft: Boolean = false,
val id: Int = 0,
val id: String = "",
override val kind: String = "",
@SerialName("label_name")
val labelName: String = "",

View File

@ -16,5 +16,5 @@ val globalJson by lazy {
* Removing Illegal Chars from File Name
* **/
fun removeIllegalChars(fileName: String): String {
return fileName.replace("[^\\dA-Za-z_]".toRegex(), "_")
return fileName.replace("[^\\dA-Za-z0-9-_]".toRegex(), "_")
}

View File

@ -5,6 +5,7 @@ import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase
import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponsePlaylist
import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponseTrack
import com.shabinder.common.utils.globalJson
import io.ktor.client.HttpClient
import io.ktor.client.features.ClientRequestException
import io.ktor.client.request.get
@ -12,7 +13,13 @@ import io.ktor.client.request.parameter
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
interface SoundCloudRequests {
@ -74,18 +81,21 @@ interface SoundCloudRequests {
if (media.transcodings.isNotEmpty())
return this
val infoURL = URLS.TRACK_INFO.buildURL(id.toString())
return httpClient.get(infoURL) {
val infoURL = URLS.TRACK_INFO.buildURL(id)
val data: String = httpClient.get(infoURL) {
parameter("client_id", CLIENT_ID)
}
return globalJson.decodeFromString(data)
}
private suspend fun getResponseObj(url: String, clientID: String = CLIENT_ID): SoundCloudResolveResponseBase {
val itemURL = URLS.RESOLVE.buildURL(url)
val resp: SoundCloudResolveResponseBase = try {
httpClient.get(itemURL) {
val data: String = httpClient.get(itemURL) {
parameter("client_id", clientID)
}
globalJson.decodeFromString(SoundCloudSerializer, data)
} catch (e: ClientRequestException) {
if (clientID != ALT_CLIENT_ID)
return getResponseObj(url, ALT_CLIENT_ID)
@ -116,6 +126,25 @@ interface SoundCloudRequests {
const val CLIENT_ID = "a3e059563d7fd3372b49b37f00a00bcf"
const val ALT_CLIENT_ID = "2t9loNQH90kzJcsFCODdigxfp325aq4z"
object SoundCloudSerializer :
JsonContentPolymorphicSerializer<SoundCloudResolveResponseBase>(SoundCloudResolveResponseBase::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out SoundCloudResolveResponseBase> =
when {
"track_count" in element.jsonObject -> SoundCloudResolveResponsePlaylist.serializer()
"kind" in element.jsonObject -> {
val isTrack =
element.jsonObject["kind"]
?.jsonPrimitive?.content.toString()
.contains("track", true)
when {
isTrack || "track_format" in element.jsonObject -> SoundCloudResolveResponseTrack.serializer()
else -> SoundCloudResolveResponsePlaylist.serializer()
}
}
else -> SoundCloudResolveResponsePlaylist.serializer()
}
}
}
}