mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-18 04:30:48 +01:00
Merge pull request #86 from ultrasonic/handle-server-versions
Adapt client to server version
This commit is contained in:
commit
491d8423c4
@ -21,6 +21,7 @@ ext.versions = [
|
||||
okhttp : "3.9.0",
|
||||
|
||||
junit : "4.12",
|
||||
mockito : "2.12.0",
|
||||
mockitoKotlin : "1.5.0",
|
||||
kluent : "1.26",
|
||||
apacheCodecs : "1.10",
|
||||
@ -53,6 +54,8 @@ ext.testing = [
|
||||
junit : "junit:junit:$versions.junit",
|
||||
kotlinJunit : "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin",
|
||||
mockitoKotlin : "com.nhaarman:mockito-kotlin:$versions.mockitoKotlin",
|
||||
mockito : "org.mockito:mockito-core:$versions.mockito",
|
||||
mockitoInline : "org.mockito:mockito-inline:$versions.mockito",
|
||||
kluent : "org.amshove.kluent:kluent:$versions.kluent",
|
||||
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp",
|
||||
apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs",
|
||||
|
@ -21,6 +21,8 @@ dependencies {
|
||||
|
||||
testImplementation testing.junit
|
||||
testImplementation testing.kotlinJunit
|
||||
testImplementation testing.mockito
|
||||
testImplementation testing.mockitoInline
|
||||
testImplementation testing.mockitoKotlin
|
||||
testImplementation testing.kluent
|
||||
testImplementation testing.mockWebServer
|
||||
|
@ -25,7 +25,7 @@ class GetStreamUrlTest {
|
||||
USERNAME, PASSWORD, V1_6_0, CLIENT_ID)
|
||||
val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString()
|
||||
expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&u=$USERNAME" +
|
||||
"&v=${V1_6_0.restApiVersion}&c=$CLIENT_ID&f=json&p=enc:${PASSWORD.toHexBytes()}"
|
||||
"&c=$CLIENT_ID&f=json&v=${V1_6_0.restApiVersion}&p=enc:${PASSWORD.toHexBytes()}"
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -30,7 +30,7 @@ class PasswordMD5InterceptorTest : BaseInterceptorTest() {
|
||||
val salt = requestLine.split('&').find { it.startsWith("s=") }
|
||||
?.substringAfter('=')?.substringBefore(" ")
|
||||
val expectedToken = String(Hex.encodeHex(MessageDigest.getInstance("MD5")
|
||||
.digest("$password$salt".toByteArray()), false))
|
||||
.digest("$password$salt".toByteArray()), true))
|
||||
requestLine `should contain` "t=$expectedToken"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
package org.moire.ultrasonic.api.subsonic.interceptors
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.amshove.kluent.`should contain`
|
||||
import org.amshove.kluent.`should equal`
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||
import org.moire.ultrasonic.api.subsonic.enqueueResponse
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
|
||||
/**
|
||||
* Integration test for [VersionInterceptor].
|
||||
*/
|
||||
class VersionInterceptorTest : BaseInterceptorTest() {
|
||||
private val initialProtocolVersion = SubsonicAPIVersions.V1_1_0
|
||||
private var updatedProtocolVersion = SubsonicAPIVersions.V1_1_0
|
||||
|
||||
override val interceptor: Interceptor by lazy(NONE) {
|
||||
VersionInterceptor(initialProtocolVersion) {
|
||||
updatedProtocolVersion = it
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should add initial protocol version to request`() {
|
||||
mockWebServerRule.enqueueResponse("ping_ok.json")
|
||||
val request = createRequest {}
|
||||
|
||||
client.newCall(request).execute()
|
||||
|
||||
val requestLine = mockWebServerRule.mockWebServer.takeRequest().requestLine
|
||||
|
||||
requestLine `should contain` "v=${initialProtocolVersion.restApiVersion}"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should update version from response`() {
|
||||
mockWebServerRule.enqueueResponse("ping_ok.json")
|
||||
|
||||
client.newCall(createRequest {}).execute()
|
||||
|
||||
(interceptor as VersionInterceptor).protocolVersion `should equal` SubsonicAPIVersions.V1_13_0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should not update version if response json doesn't contain version`() {
|
||||
mockWebServerRule.enqueueResponse("non_subsonic_response.json")
|
||||
|
||||
client.newCall(createRequest {}).execute()
|
||||
|
||||
(interceptor as VersionInterceptor).protocolVersion `should equal` initialProtocolVersion
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should not update version on non-json response`() {
|
||||
mockWebServerRule.mockWebServer.enqueue(MockResponse()
|
||||
.setBody("asdqwnekjnqwkjen")
|
||||
.setHeader("Content-Type", "application/octet-stream"))
|
||||
|
||||
client.newCall(createRequest {}).execute()
|
||||
|
||||
(interceptor as VersionInterceptor).protocolVersion `should equal` initialProtocolVersion
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should notify notifier on version change`() {
|
||||
mockWebServerRule.enqueueResponse("ping_ok.json")
|
||||
|
||||
client.newCall(createRequest {}).execute()
|
||||
|
||||
updatedProtocolVersion `should equal` SubsonicAPIVersions.V1_13_0
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"none" : "some"
|
||||
}
|
@ -9,7 +9,9 @@ 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 org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
||||
import retrofit2.Response
|
||||
@ -28,10 +30,25 @@ private const val READ_TIMEOUT = 60_000L
|
||||
*/
|
||||
class SubsonicAPIClient(baseUrl: String,
|
||||
username: String,
|
||||
private val password: String,
|
||||
clientProtocolVersion: SubsonicAPIVersions,
|
||||
password: String,
|
||||
minimalProtocolVersion: SubsonicAPIVersions,
|
||||
clientID: String,
|
||||
debug: Boolean = false) {
|
||||
private val versionInterceptor = VersionInterceptor(minimalProtocolVersion) {
|
||||
protocolVersion = it
|
||||
}
|
||||
private val proxyPasswordInterceptor = ProxyPasswordInterceptor(minimalProtocolVersion,
|
||||
PasswordHexInterceptor(password), PasswordMD5Interceptor(password))
|
||||
|
||||
/**
|
||||
* Get currently used protocol version.
|
||||
*/
|
||||
var protocolVersion = minimalProtocolVersion
|
||||
private set(value) {
|
||||
field = value
|
||||
proxyPasswordInterceptor.apiVersion = field
|
||||
}
|
||||
|
||||
private val okHttpClient = OkHttpClient.Builder()
|
||||
.readTimeout(READ_TIMEOUT, MILLISECONDS)
|
||||
.addInterceptor { chain ->
|
||||
@ -39,15 +56,15 @@ class SubsonicAPIClient(baseUrl: String,
|
||||
val originalRequest = chain.request()
|
||||
val newUrl = originalRequest.url().newBuilder()
|
||||
.addQueryParameter("u", username)
|
||||
.addQueryParameter("v", clientProtocolVersion.restApiVersion)
|
||||
.addQueryParameter("c", clientID)
|
||||
.addQueryParameter("f", "json")
|
||||
.build()
|
||||
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
|
||||
}
|
||||
.addInterceptor(versionInterceptor)
|
||||
.addInterceptor(proxyPasswordInterceptor)
|
||||
.addInterceptor(RangeHeaderInterceptor())
|
||||
.apply { if (debug) addLogging() }
|
||||
.addPasswordQueryParam(clientProtocolVersion)
|
||||
.build()
|
||||
|
||||
private val jacksonMapper = ObjectMapper()
|
||||
@ -139,14 +156,4 @@ class SubsonicAPIClient(baseUrl: String,
|
||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
this.addInterceptor(loggingInterceptor)
|
||||
}
|
||||
|
||||
private fun OkHttpClient.Builder.addPasswordQueryParam(
|
||||
clientProtocolVersion: SubsonicAPIVersions): OkHttpClient.Builder {
|
||||
if (clientProtocolVersion < SubsonicAPIVersions.V1_13_0) {
|
||||
this.addInterceptor(PasswordHexInterceptor(password))
|
||||
} else {
|
||||
this.addInterceptor(PasswordMD5Interceptor(password))
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
@ -26,10 +26,11 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
||||
V1_12_0("5.2", "1.12.0"),
|
||||
V1_13_0("5.3", "1.13.0"),
|
||||
V1_14_0("6.0", "1.14.0"),
|
||||
V1_15_0("6.1", "1.15.0");
|
||||
V1_15_0("6.1", "1.15.0"),
|
||||
V1_16_0("6.1.2", "1.16.0");
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@JvmStatic @Throws(IllegalArgumentException::class)
|
||||
fun fromApiVersion(apiVersion: String): SubsonicAPIVersions {
|
||||
when (apiVersion) {
|
||||
"1.1.0" -> return V1_1_0
|
||||
@ -48,8 +49,9 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
||||
"1.13.0" -> return V1_13_0
|
||||
"1.14.0" -> return V1_14_0
|
||||
"1.15.0" -> return V1_15_0
|
||||
"1.16.0" -> return V1_16_0
|
||||
else -> throw IllegalArgumentException("Unknown api version $apiVersion")
|
||||
}
|
||||
throw IllegalArgumentException("Unknown api version $apiVersion")
|
||||
}
|
||||
|
||||
class SubsonicAPIVersionsDeserializer : JsonDeserializer<SubsonicAPIVersions>() {
|
||||
|
@ -23,7 +23,7 @@ class PasswordMD5Interceptor(private val password: String) : Interceptor {
|
||||
private val passwordMD5Hash: String by lazy {
|
||||
try {
|
||||
val md5Digest = MessageDigest.getInstance("MD5")
|
||||
md5Digest.digest("$password$salt".toByteArray()).toHexBytes()
|
||||
md5Digest.digest("$password$salt".toByteArray()).toHexBytes().toLowerCase()
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IllegalStateException(e)
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
package org.moire.ultrasonic.api.subsonic.interceptors
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Interceptor.Chain
|
||||
import okhttp3.Response
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||
|
||||
/**
|
||||
* Proxy [Interceptor] that uses one of [hexInterceptor] or [mD5Interceptor] depends on [apiVersion].
|
||||
*/
|
||||
internal class ProxyPasswordInterceptor(
|
||||
initialAPIVersions: SubsonicAPIVersions,
|
||||
private val hexInterceptor: PasswordHexInterceptor,
|
||||
private val mD5Interceptor: PasswordMD5Interceptor) : Interceptor {
|
||||
var apiVersion: SubsonicAPIVersions = initialAPIVersions
|
||||
|
||||
override fun intercept(chain: Chain): Response =
|
||||
if (apiVersion < SubsonicAPIVersions.V1_13_0) {
|
||||
hexInterceptor.intercept(chain)
|
||||
} else {
|
||||
mD5Interceptor.intercept(chain)
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package org.moire.ultrasonic.api.subsonic.interceptors
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Interceptor.Chain
|
||||
import okhttp3.Response
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||
import java.io.IOException
|
||||
|
||||
private const val DEFAULT_PEEK_BYTE_COUNT = 100L
|
||||
|
||||
/**
|
||||
* Special [Interceptor] that adds client supported version to request and tries to update it
|
||||
* from server response.
|
||||
*
|
||||
* Optionally [notifier] will be invoked on version change.
|
||||
*
|
||||
* @author Yahor Berdnikau
|
||||
*/
|
||||
internal class VersionInterceptor(
|
||||
internal var protocolVersion: SubsonicAPIVersions,
|
||||
private val notifier: (SubsonicAPIVersions) -> Unit = {}) : Interceptor {
|
||||
private val jsonFactory = JsonFactory()
|
||||
|
||||
override fun intercept(chain: Chain): okhttp3.Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val newRequest = originalRequest.newBuilder()
|
||||
.url(originalRequest
|
||||
.url()
|
||||
.newBuilder()
|
||||
.addQueryParameter("v", protocolVersion.restApiVersion)
|
||||
.build())
|
||||
.build()
|
||||
|
||||
val response = chain.proceed(newRequest)
|
||||
if (response.isSuccessful) {
|
||||
val isJson = response.body()?.contentType()?.subtype()?.equals("json", true) ?: false
|
||||
if (isJson) {
|
||||
tryUpdateProtocolVersion(response)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private fun tryUpdateProtocolVersion(response: Response) {
|
||||
val content = response.peekBody(DEFAULT_PEEK_BYTE_COUNT)
|
||||
.byteStream().bufferedReader().readText()
|
||||
|
||||
try {
|
||||
val jsonReader = jsonFactory.createParser(content)
|
||||
jsonReader.nextToken()
|
||||
if (jsonReader.currentToken == JsonToken.START_OBJECT) {
|
||||
while (jsonReader.currentName != "version" &&
|
||||
jsonReader.currentToken != null) {
|
||||
jsonReader.nextToken()
|
||||
}
|
||||
val versionStr = jsonReader.nextTextValue()
|
||||
if (versionStr != null) {
|
||||
try {
|
||||
protocolVersion = SubsonicAPIVersions.fromApiVersion(versionStr)
|
||||
notifier(protocolVersion)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (io: IOException) {
|
||||
// no-op
|
||||
} catch (parse: JsonParseException) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.moire.ultrasonic.api.subsonic.interceptors
|
||||
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.verify
|
||||
import okhttp3.Interceptor.Chain
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_12_0
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_13_0
|
||||
|
||||
/**
|
||||
* Unit test for [ProxyPasswordInterceptor].
|
||||
*/
|
||||
class ProxyPasswordInterceptorTest {
|
||||
private val mockPasswordHexInterceptor = mock<PasswordHexInterceptor>()
|
||||
private val mockPasswordMd5Interceptor = mock<PasswordMD5Interceptor>()
|
||||
private val mockChain = mock<Chain>()
|
||||
|
||||
private val proxyInterceptor = ProxyPasswordInterceptor(V1_12_0,
|
||||
mockPasswordHexInterceptor, mockPasswordMd5Interceptor)
|
||||
|
||||
@Test
|
||||
fun `Should use hex password on versions less then 1 13 0`() {
|
||||
proxyInterceptor.intercept(mockChain)
|
||||
|
||||
verify(mockPasswordHexInterceptor).intercept(mockChain)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should use md5 password on version 1 13 0`() {
|
||||
proxyInterceptor.apiVersion = V1_13_0
|
||||
|
||||
proxyInterceptor.intercept(mockChain)
|
||||
|
||||
verify(mockPasswordMd5Interceptor).intercept(mockChain)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user