package org.moire.ultrasonic.api.subsonic import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import java.security.SecureRandom import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit.MILLISECONDS import javax.net.ssl.SSLContext import javax.net.ssl.X509TrustManager import okhttp3.OkHttpClient import okhttp3.ResponseBody import okhttp3.logging.HttpLoggingInterceptor import org.moire.ultrasonic.api.subsonic.interceptors.PasswordHexInterceptor import org.moire.ultrasonic.api.subsonic.interceptors.PasswordMD5Interceptor import org.moire.ultrasonic.api.subsonic.interceptors.ProxyPasswordInterceptor import org.moire.ultrasonic.api.subsonic.interceptors.RangeHeaderInterceptor import org.moire.ultrasonic.api.subsonic.interceptors.VersionInterceptor import org.moire.ultrasonic.api.subsonic.response.StreamResponse import retrofit2.Response import retrofit2.Retrofit private const val READ_TIMEOUT = 60_000L /** * Subsonic API client that provides api access. * * For supported API calls see [SubsonicAPIDefinition]. * * Client will automatically adjust [protocolVersion] to the current server version on * doing successful requests. * * @author Yahor Berdnikau */ class SubsonicAPIClient( config: SubsonicClientConfiguration, private val okLogger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger.DEFAULT, baseOkClient: OkHttpClient = OkHttpClient.Builder().build() ) { private val versionInterceptor = VersionInterceptor(config.minimalProtocolVersion) private val proxyPasswordInterceptor = ProxyPasswordInterceptor( config.minimalProtocolVersion, PasswordHexInterceptor(config.password), PasswordMD5Interceptor(config.password), config.enableLdapUserSupport ) var onProtocolChange: (SubsonicAPIVersions) -> Unit = {} /** * The currently used protocol version. * The setter also updates the interceptors and callback (if registered) */ var protocolVersion = config.minimalProtocolVersion private set(value) { field = value proxyPasswordInterceptor.apiVersion = field wrappedApi.currentApiVersion = field wrappedApi.isRealProtocolVersion = true versionInterceptor.protocolVersion = field onProtocolChange(field) } val okHttpClient: OkHttpClient = baseOkClient.newBuilder() .readTimeout(READ_TIMEOUT, MILLISECONDS) .apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() } .addInterceptor { chain -> // Adds default request params val originalRequest = chain.request() val newUrl = originalRequest.url().newBuilder() .addQueryParameter("u", config.username) .addQueryParameter("c", config.clientID) .addQueryParameter("f", "json") .build() chain.proceed(originalRequest.newBuilder().url(newUrl).build()) } .addInterceptor(versionInterceptor) .addInterceptor(proxyPasswordInterceptor) .addInterceptor(RangeHeaderInterceptor()) .apply { if (config.debug) addLogging() } .build() // Create the Retrofit instance, and register a special converter factory // It will update our protocol version to the correct version, once we made a successful call private val retrofit: Retrofit = Retrofit.Builder() .baseUrl("${config.baseUrl}/rest/") .client(okHttpClient) .addConverterFactory( VersionAwareJacksonConverterFactory.create( { // Only trigger update on change, or if still using the default if (protocolVersion != it || !config.isRealProtocolVersion) { protocolVersion = it } }, jacksonMapper ) ) .build() private val wrappedApi = ApiVersionCheckWrapper( retrofit.create(SubsonicAPIDefinition::class.java), config.minimalProtocolVersion, config.isRealProtocolVersion ) val api: SubsonicAPIDefinition get() = wrappedApi private fun OkHttpClient.Builder.addLogging() { val loggingInterceptor = HttpLoggingInterceptor(okLogger) loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY this.addInterceptor(loggingInterceptor) } private fun OkHttpClient.Builder.allowSelfSignedCertificates() { val trustManager = @Suppress("CustomX509TrustManager") object : X509TrustManager { @Suppress("TrustAllX509TrustManager") override fun checkClientTrusted(p0: Array?, p1: String?) {} @Suppress("TrustAllX509TrustManager") override fun checkServerTrusted(p0: Array?, p1: String?) {} override fun getAcceptedIssuers(): Array = emptyArray() } val sslContext = SSLContext.getInstance("SSL") sslContext.init(null, arrayOf(trustManager), SecureRandom()) sslSocketFactory(sslContext.socketFactory, trustManager) hostnameVerifier { _, _ -> true } } /** * This function is necessary because Mockito has problems with stubbing chained calls */ fun toStreamResponse(call: Response): StreamResponse { return call.toStreamResponse() } companion object { val jacksonMapper: ObjectMapper = ObjectMapper() .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) .registerModule(KotlinModule()) } }