Fixed Subsonic API version detection

Fixed server change detection
Minor fixes
This commit is contained in:
Nite 2020-10-13 21:41:01 +02:00
parent 356af198e0
commit a396b4b27b
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
11 changed files with 100 additions and 32 deletions

View File

@ -1,9 +1,9 @@
package org.moire.ultrasonic.api.subsonic.interceptors package org.moire.ultrasonic.api.subsonic.interceptors
import java.math.BigInteger
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
import java.util.Locale
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Interceptor.Chain import okhttp3.Interceptor.Chain
import okhttp3.Response import okhttp3.Response
@ -15,27 +15,33 @@ import okhttp3.Response
* and above. * and above.
*/ */
class PasswordMD5Interceptor(private val password: String) : Interceptor { class PasswordMD5Interceptor(private val password: String) : Interceptor {
private val salt: String by lazy { private val secureRandom = SecureRandom()
val secureRandom = SecureRandom() private val saltBytes = ByteArray(16)
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)
}
}
override fun intercept(chain: Chain): Response { override fun intercept(chain: Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val salt = getSalt()
val updatedUrl = originalRequest.url().newBuilder() val updatedUrl = originalRequest.url().newBuilder()
.addQueryParameter("t", passwordMD5Hash) .addQueryParameter("t", getPasswordMD5Hash(salt))
.addQueryParameter("s", salt) .addQueryParameter("s", salt)
.build() .build()
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).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)
}
}
} }

View File

@ -34,6 +34,7 @@ import android.widget.TextView;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.data.ServerSetting;
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport; import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
@ -55,7 +56,7 @@ public class MainActivity extends SubsonicTabActivity
{ {
private static boolean infoDialogDisplayed; private static boolean infoDialogDisplayed;
private static boolean shouldUseId3; private static boolean shouldUseId3;
private static int lastActiveServer; private static String lastActiveServerProperties;
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class); private Lazy<ActiveServerProvider> 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 albumsAlphaByArtistButton = buttons.findViewById(R.id.main_albums_alphaByArtist);
final View videosButton = buttons.findViewById(R.id.main_videos); final View videosButton = buttons.findViewById(R.id.main_videos);
lastActiveServer = ActiveServerProvider.Companion.getActiveServerId(this); lastActiveServerProperties = getActiveServerProperties();
String name = activeServerProvider.getValue().getActiveServer().getName(); String name = activeServerProvider.getValue().getActiveServer().getName();
serverTextView.setText(name); serverTextView.setText(name);
@ -260,7 +261,7 @@ public class MainActivity extends SubsonicTabActivity
boolean shouldRestart = false; boolean shouldRestart = false;
boolean id3 = Util.getShouldUseId3Tags(MainActivity.this); boolean id3 = Util.getShouldUseId3Tags(MainActivity.this);
int currentActiveServer = ActiveServerProvider.Companion.getActiveServerId(MainActivity.this); String currentActiveServerProperties = getActiveServerProperties();
if (id3 != shouldUseId3) if (id3 != shouldUseId3)
{ {
@ -268,9 +269,9 @@ public class MainActivity extends SubsonicTabActivity
shouldRestart = true; shouldRestart = true;
} }
if (currentActiveServer != lastActiveServer) if (!currentActiveServerProperties.equals(lastActiveServerProperties))
{ {
lastActiveServer = currentActiveServer; lastActiveServerProperties = currentActiveServerProperties;
shouldRestart = true; shouldRestart = true;
} }
@ -378,6 +379,14 @@ public class MainActivity extends SubsonicTabActivity
startActivityForResult(intent, 0); 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. * Temporary task to make a ping to server to get it supported api version.
*/ */

View File

@ -141,6 +141,20 @@ public class RESTMusicService implements MusicService {
public void ping(Context context, ProgressListener progressListener) throws Exception { public void ping(Context context, ProgressListener progressListener) throws Exception {
updateProgressListener(progressListener, R.string.service_connecting); updateProgressListener(progressListener, R.string.service_connecting);
if (activeServerProvider.getValue().getActiveServer().getMinimumApiVersion() == null) {
try {
final Response<SubsonicResponse> 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<SubsonicResponse> response = subsonicAPIClient.getApi().ping().execute(); final Response<SubsonicResponse> response = subsonicAPIClient.getApi().ping().execute();
checkResponseSuccessful(response); checkResponseSuccessful(response);
} }

View File

@ -260,7 +260,17 @@ internal class EditServerActivity : AppCompatActivity() {
BuildConfig.DEBUG BuildConfig.DEBUG
) )
val subsonicApiClient = SubsonicAPIClient(configuration) 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) checkResponseSuccessful(pingResponse)
val licenseResponse = subsonicApiClient.api.getLicense().execute() val licenseResponse = subsonicApiClient.api.getLicense().execute()

View File

@ -209,7 +209,8 @@ class ServerSettingsModel(
false false
), ),
settings.getBoolean(PREFERENCES_KEY_LDAP_SUPPORT + preferenceId, 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
) )
} }

View File

@ -57,7 +57,8 @@ class ActiveServerProvider(
jukeboxByDefault = false, jukeboxByDefault = false,
allowSelfSignedCertificate = false, allowSelfSignedCertificate = false,
ldapSupport = 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 * Invalidates the Active Server Setting cache
* This should be called when the Active Server or one of its properties changes * This should be called when the Active Server or one of its properties changes

View File

@ -2,11 +2,13 @@ package org.moire.ultrasonic.data
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
/** /**
* Room Database to be used to store data for Ultrasonic * 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() { abstract class AppDatabase : RoomDatabase() {
/** /**
@ -14,3 +16,11 @@ abstract class AppDatabase : RoomDatabase() {
*/ */
abstract fun serverSettingDao(): ServerSettingDao 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"
)
}
}

View File

@ -28,12 +28,13 @@ data class ServerSetting(
@ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: Boolean, @ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: Boolean,
@ColumnInfo(name = "allowSelfSignedCertificate") var allowSelfSignedCertificate: Boolean, @ColumnInfo(name = "allowSelfSignedCertificate") var allowSelfSignedCertificate: Boolean,
@ColumnInfo(name = "ldapSupport") var ldapSupport: 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 ( constructor() : this (
-1, 0, "", "", "", "", false, false, false, null -1, 0, "", "", "", "", false, false, false, null, null
) )
constructor(name: String, url: String) : this( constructor(name: String, url: String) : this(
-1, 0, name, url, "", "", false, false, false, null -1, 0, name, url, "", "", false, false, false, null, null
) )
} }

View File

@ -8,6 +8,7 @@ import org.koin.dsl.module
import org.moire.ultrasonic.activity.ServerSettingsModel import org.moire.ultrasonic.activity.ServerSettingsModel
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.AppDatabase import org.moire.ultrasonic.data.AppDatabase
import org.moire.ultrasonic.data.MIGRATION_1_2
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
const val SP_NAME = "Default_SP" const val SP_NAME = "Default_SP"
@ -20,7 +21,9 @@ val appPermanentStorage = module {
androidContext(), androidContext(),
AppDatabase::class.java, AppDatabase::class.java,
"ultrasonic-database" "ultrasonic-database"
).build() )
.addMigrations(MIGRATION_1_2)
.build()
} }
single { get<AppDatabase>().serverSettingDao() } single { get<AppDatabase>().serverSettingDao() }

View File

@ -46,7 +46,8 @@ val musicServiceModule = module {
username = get<ActiveServerProvider>().getActiveServer().userName, username = get<ActiveServerProvider>().getActiveServer().userName,
password = get<ActiveServerProvider>().getActiveServer().password, password = get<ActiveServerProvider>().getActiveServer().password,
minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion( minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion(
Constants.REST_PROTOCOL_VERSION get<ActiveServerProvider>().getActiveServer().minimumApiVersion
?: Constants.REST_PROTOCOL_VERSION
), ),
clientID = Constants.REST_CLIENT_ID, clientID = Constants.REST_CLIENT_ID,
allowSelfSignedCertificate = get<ActiveServerProvider>() allowSelfSignedCertificate = get<ActiveServerProvider>()

View File

@ -309,9 +309,9 @@
<string name="settings.theme_black">Black</string> <string name="settings.theme_black">Black</string>
<string name="settings.theme_title">Theme</string> <string name="settings.theme_title">Theme</string>
<string name="settings.title.allow_self_signed_certificate">Allow self-signed HTTPS certificate</string> <string name="settings.title.allow_self_signed_certificate">Allow self-signed HTTPS certificate</string>
<string name="settings.title.enable_ldap_users_support">Enable support for LDAP users</string> <string name="settings.title.enable_ldap_users_support">Force plain password authentication</string>
<string name="settings.summary.enable_ldap_users_support">This forces app to always send password in old-way, <string name="settings.summary.enable_ldap_users_support">This forces the app to always send the password unencrypted.
because Subsonic api does not support new authorization for LDAP users.</string> Useful if the Subsonic server does not support the new authentication API for the users.</string>
<string name="settings.use_folder_for_album_artist">Use Folders For Artist Name</string> <string name="settings.use_folder_for_album_artist">Use Folders For Artist Name</string>
<string name="settings.use_folder_for_album_artist_summary">Assume top-level folder is the name of the album artist</string> <string name="settings.use_folder_for_album_artist_summary">Assume top-level folder is the name of the album artist</string>
<string name="settings.use_id3">Browse Using ID3 Tags</string> <string name="settings.use_id3">Browse Using ID3 Tags</string>