1
0
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:
Yahor Berdnikau 2017-12-02 21:58:37 +01:00 committed by GitHub
commit 491d8423c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 247 additions and 20 deletions

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,3 @@
{
"none" : "some"
}

View File

@ -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
}
}

View File

@ -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>() {

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}