diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/PasswordMD5Interceptor.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/PasswordMD5Interceptor.kt index 5596c7b0..75d6a48e 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/PasswordMD5Interceptor.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/PasswordMD5Interceptor.kt @@ -1,9 +1,9 @@ package org.moire.ultrasonic.api.subsonic.interceptors -import java.math.BigInteger import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.security.SecureRandom +import java.util.Locale import okhttp3.Interceptor import okhttp3.Interceptor.Chain import okhttp3.Response @@ -15,27 +15,33 @@ import okhttp3.Response * and above. */ class PasswordMD5Interceptor(private val password: String) : Interceptor { - private val salt: String by lazy { - val secureRandom = SecureRandom() - BigInteger(130, secureRandom).toString(32) - } - - private val passwordMD5Hash: String by lazy { - try { - val md5Digest = MessageDigest.getInstance("MD5") - md5Digest.digest("$password$salt".toByteArray()).toHexBytes().toLowerCase() - } catch (e: NoSuchAlgorithmException) { - throw IllegalStateException(e) - } - } + private val secureRandom = SecureRandom() + private val saltBytes = ByteArray(16) override fun intercept(chain: Chain): Response { val originalRequest = chain.request() + val salt = getSalt() val updatedUrl = originalRequest.url().newBuilder() - .addQueryParameter("t", passwordMD5Hash) + .addQueryParameter("t", getPasswordMD5Hash(salt)) .addQueryParameter("s", salt) .build() return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build()) } + + private fun getSalt(): String { + secureRandom.nextBytes(saltBytes) + return saltBytes.toHexBytes() + } + + private fun getPasswordMD5Hash(salt: String): String { + try { + val md5Digest = MessageDigest.getInstance("MD5") + return md5Digest.digest( + "$password$salt".toByteArray() + ).toHexBytes().toLowerCase(Locale.getDefault()) + } catch (e: NoSuchAlgorithmException) { + throw IllegalStateException(e) + } + } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java index 83237def..6948927a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java @@ -34,6 +34,7 @@ import android.widget.TextView; import org.moire.ultrasonic.R; import org.moire.ultrasonic.data.ActiveServerProvider; +import org.moire.ultrasonic.data.ServerSetting; import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; @@ -55,7 +56,7 @@ public class MainActivity extends SubsonicTabActivity { private static boolean infoDialogDisplayed; private static boolean shouldUseId3; - private static int lastActiveServer; + private static String lastActiveServerProperties; private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); private Lazy activeServerProvider = inject(ActiveServerProvider.class); @@ -121,7 +122,7 @@ public class MainActivity extends SubsonicTabActivity final View albumsAlphaByArtistButton = buttons.findViewById(R.id.main_albums_alphaByArtist); final View videosButton = buttons.findViewById(R.id.main_videos); - lastActiveServer = ActiveServerProvider.Companion.getActiveServerId(this); + lastActiveServerProperties = getActiveServerProperties(); String name = activeServerProvider.getValue().getActiveServer().getName(); serverTextView.setText(name); @@ -260,7 +261,7 @@ public class MainActivity extends SubsonicTabActivity boolean shouldRestart = false; boolean id3 = Util.getShouldUseId3Tags(MainActivity.this); - int currentActiveServer = ActiveServerProvider.Companion.getActiveServerId(MainActivity.this); + String currentActiveServerProperties = getActiveServerProperties(); if (id3 != shouldUseId3) { @@ -268,9 +269,9 @@ public class MainActivity extends SubsonicTabActivity shouldRestart = true; } - if (currentActiveServer != lastActiveServer) + if (!currentActiveServerProperties.equals(lastActiveServerProperties)) { - lastActiveServer = currentActiveServer; + lastActiveServerProperties = currentActiveServerProperties; shouldRestart = true; } @@ -378,6 +379,14 @@ public class MainActivity extends SubsonicTabActivity startActivityForResult(intent, 0); } + private String getActiveServerProperties() + { + ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer(); + return String.format("%s;%s;%s;%s;%s;%s", currentSetting.getUrl(), currentSetting.getUserName(), + currentSetting.getPassword(), currentSetting.getAllowSelfSignedCertificate(), + currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion()); + } + /** * Temporary task to make a ping to server to get it supported api version. */ diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java index 7c866915..0c2ab55c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -141,6 +141,20 @@ public class RESTMusicService implements MusicService { public void ping(Context context, ProgressListener progressListener) throws Exception { updateProgressListener(progressListener, R.string.service_connecting); + if (activeServerProvider.getValue().getActiveServer().getMinimumApiVersion() == null) { + try { + final Response response = subsonicAPIClient.getApi().ping().execute(); + if (response != null && response.body() != null) { + String restApiVersion = response.body().getVersion().getRestApiVersion(); + Timber.i("Server minimum API version set to %s", restApiVersion); + activeServerProvider.getValue().setMinimumApiVersion(restApiVersion); + } + } catch (Exception ignored) { + // This Ping is only used to get the API Version, if it fails, that's no problem. + } + } + + // This Ping will be now executed with the correct API Version, so it shouldn't fail final Response response = subsonicAPIClient.getApi().ping().execute(); checkResponseSuccessful(response); } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt index 7e039f3a..dd6b5ef8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt @@ -260,7 +260,17 @@ internal class EditServerActivity : AppCompatActivity() { BuildConfig.DEBUG ) val subsonicApiClient = SubsonicAPIClient(configuration) - val pingResponse = subsonicApiClient.api.ping().execute() + + // Execute a ping to retrieve the API version. This is accepted to fail if the authentication is incorrect yet. + var pingResponse = subsonicApiClient.api.ping().execute() + if (pingResponse?.body() != null) { + val restApiVersion = pingResponse.body()!!.version.restApiVersion + currentServerSetting!!.minimumApiVersion = restApiVersion + Timber.i("Server minimum API version set to %s", restApiVersion) + } + + // Execute a ping to check the authentication, now using the correct API version. + pingResponse = subsonicApiClient.api.ping().execute() checkResponseSuccessful(pingResponse) val licenseResponse = subsonicApiClient.api.getLicense().execute() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt index 6de6aef2..5357a9f5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt @@ -209,7 +209,8 @@ class ServerSettingsModel( false ), settings.getBoolean(PREFERENCES_KEY_LDAP_SUPPORT + preferenceId, false), - settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + preferenceId, null) + settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + preferenceId, null), + null ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index 08aa4fce..5f7a2e9d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -57,7 +57,8 @@ class ActiveServerProvider( jukeboxByDefault = false, allowSelfSignedCertificate = false, ldapSupport = false, - musicFolderId = "" + musicFolderId = "", + minimumApiVersion = null ) } @@ -79,6 +80,18 @@ class ActiveServerProvider( } } + /** + * Sets the minimum Subsonic API version of the current server. + */ + fun setMinimumApiVersion(apiVersion: String) { + GlobalScope.launch(Dispatchers.IO) { + if (cachedServer != null) { + cachedServer!!.minimumApiVersion = apiVersion + repository.update(cachedServer!!) + } + } + } + /** * Invalidates the Active Server Setting cache * This should be called when the Active Server or one of its properties changes diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt index f9704f26..ee265000 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt @@ -2,11 +2,13 @@ package org.moire.ultrasonic.data import androidx.room.Database import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase /** * Room Database to be used to store data for Ultrasonic */ -@Database(entities = [ServerSetting::class], version = 1) +@Database(entities = [ServerSetting::class], version = 2) abstract class AppDatabase : RoomDatabase() { /** @@ -14,3 +16,11 @@ abstract class AppDatabase : RoomDatabase() { */ abstract fun serverSettingDao(): ServerSettingDao } + +val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "ALTER TABLE ServerSetting ADD COLUMN minimumApiVersion TEXT" + ) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt index 5c337142..c5749a23 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt @@ -28,12 +28,13 @@ data class ServerSetting( @ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: Boolean, @ColumnInfo(name = "allowSelfSignedCertificate") var allowSelfSignedCertificate: Boolean, @ColumnInfo(name = "ldapSupport") var ldapSupport: Boolean, - @ColumnInfo(name = "musicFolderId") var musicFolderId: String? + @ColumnInfo(name = "musicFolderId") var musicFolderId: String?, + @ColumnInfo(name = "minimumApiVersion") var minimumApiVersion: String? ) { constructor() : this ( - -1, 0, "", "", "", "", false, false, false, null + -1, 0, "", "", "", "", false, false, false, null, null ) constructor(name: String, url: String) : this( - -1, 0, name, url, "", "", false, false, false, null + -1, 0, name, url, "", "", false, false, false, null, null ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt index 34ae53f2..b54102e0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt @@ -8,6 +8,7 @@ import org.koin.dsl.module import org.moire.ultrasonic.activity.ServerSettingsModel import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.AppDatabase +import org.moire.ultrasonic.data.MIGRATION_1_2 import org.moire.ultrasonic.util.Util const val SP_NAME = "Default_SP" @@ -20,7 +21,9 @@ val appPermanentStorage = module { androidContext(), AppDatabase::class.java, "ultrasonic-database" - ).build() + ) + .addMigrations(MIGRATION_1_2) + .build() } single { get().serverSettingDao() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 72b67e43..be2431d3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -46,7 +46,8 @@ val musicServiceModule = module { username = get().getActiveServer().userName, password = get().getActiveServer().password, minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion( - Constants.REST_PROTOCOL_VERSION + get().getActiveServer().minimumApiVersion + ?: Constants.REST_PROTOCOL_VERSION ), clientID = Constants.REST_CLIENT_ID, allowSelfSignedCertificate = get() diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 4e7c1a6c..af36f428 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -309,9 +309,9 @@ Black Theme Allow self-signed HTTPS certificate - Enable support for LDAP users - This forces app to always send password in old-way, - because Subsonic api does not support new authorization for LDAP users. + Force plain password authentication + This forces the app to always send the password unencrypted. + Useful if the Subsonic server does not support the new authentication API for the users. Use Folders For Artist Name Assume top-level folder is the name of the album artist Browse Using ID3 Tags