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

View File

@ -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<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.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 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.
*/

View File

@ -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<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();
checkResponseSuccessful(response);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<AppDatabase>().serverSettingDao() }

View File

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

View File

@ -309,9 +309,9 @@
<string name="settings.theme_black">Black</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.enable_ldap_users_support">Enable support for LDAP users</string>
<string name="settings.summary.enable_ldap_users_support">This forces app to always send password in old-way,
because Subsonic api does not support new authorization 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 the app to always send the password unencrypted.
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_summary">Assume top-level folder is the name of the album artist</string>
<string name="settings.use_id3">Browse Using ID3 Tags</string>