Merge branch 'master' into 'master'
Added subsonic server config for salt auth logic See merge request sublime-music/sublime-music!47
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
@@ -5,6 +6,7 @@ import multiprocessing
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import string
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
@@ -111,6 +113,15 @@ class SubsonicAdapter(Adapter):
|
||||
helptext="If toggled, Sublime Music will periodically save the play "
|
||||
"queue state so that you can resume on other devices.",
|
||||
),
|
||||
"salt_auth": ConfigParamDescriptor(
|
||||
bool,
|
||||
"Use Salt Authentication",
|
||||
default=True,
|
||||
advanced=True,
|
||||
helptext="If toggled, Sublime Music will use salted hash tokens "
|
||||
"instead of the plain password in the request urls (only supported on "
|
||||
"Subsonic API 1.13.0+)",
|
||||
),
|
||||
}
|
||||
|
||||
if networkmanager_imported:
|
||||
@@ -160,10 +171,38 @@ class SubsonicAdapter(Adapter):
|
||||
"Double check the server address."
|
||||
)
|
||||
except ServerError as e:
|
||||
errors["__ping__"] = (
|
||||
"<b>Error connecting to the server.</b>\n"
|
||||
f"Error {e.status_code}: {str(e)}"
|
||||
)
|
||||
if e.status_code in [10, 41] and config_store["salt_auth"]:
|
||||
# status code 10: if salt auth is not enabled, server will
|
||||
# return error server error with status_code 10 since it'll
|
||||
# interpret it as a missing (password) parameter
|
||||
# status code 41: as per subsonic api docs, description of
|
||||
# status_code 41 is "Token authentication not supported for LDAP
|
||||
# users." so fall back to password auth
|
||||
try:
|
||||
config_store["salt_auth"] = False
|
||||
tmp_adapter = SubsonicAdapter(
|
||||
config_store, Path(tmp_dir_name)
|
||||
)
|
||||
tmp_adapter._get_json(
|
||||
tmp_adapter._make_url("ping"),
|
||||
timeout=2,
|
||||
is_exponential_backoff_ping=True,
|
||||
)
|
||||
logging.warn(
|
||||
"Salted auth not supported, falling back to regular "
|
||||
"password auth"
|
||||
)
|
||||
except ServerError as retry_e:
|
||||
config_store["salt_auth"] = True
|
||||
errors["__ping__"] = (
|
||||
"<b>Error connecting to the server.</b>\n"
|
||||
f"Error {retry_e.status_code}: {str(retry_e)}"
|
||||
)
|
||||
else:
|
||||
errors["__ping__"] = (
|
||||
"<b>Error connecting to the server.</b>\n"
|
||||
f"Error {e.status_code}: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
errors["__ping__"] = str(e)
|
||||
|
||||
@@ -173,7 +212,8 @@ class SubsonicAdapter(Adapter):
|
||||
|
||||
@staticmethod
|
||||
def migrate_configuration(config_store: ConfigurationStore):
|
||||
pass
|
||||
if "salt_auth" not in config_store:
|
||||
config_store["salt_auth"] = True
|
||||
|
||||
def __init__(self, config: ConfigurationStore, data_directory: Path):
|
||||
self.data_directory = data_directory
|
||||
@@ -212,6 +252,7 @@ class SubsonicAdapter(Adapter):
|
||||
self.username = config["username"]
|
||||
self.password = cast(str, config.get_secret("password"))
|
||||
self.verify_cert = config["verify_cert"]
|
||||
self.use_salt_auth = config["salt_auth"]
|
||||
|
||||
self.is_shutting_down = False
|
||||
self._ping_process: Optional[multiprocessing.Process] = None
|
||||
@@ -341,14 +382,31 @@ class SubsonicAdapter(Adapter):
|
||||
Gets the parameters that are needed for all requests to the Subsonic API. See
|
||||
Subsonic API Introduction for details.
|
||||
"""
|
||||
return {
|
||||
params = {
|
||||
"u": self.username,
|
||||
"p": self.password,
|
||||
"c": "Sublime Music",
|
||||
"f": "json",
|
||||
"v": self._version.value.decode() or "1.8.0",
|
||||
}
|
||||
|
||||
if self.use_salt_auth:
|
||||
salt, token = self._generate_auth_token()
|
||||
params["s"] = salt
|
||||
params["t"] = token
|
||||
else:
|
||||
params["p"] = self.password
|
||||
|
||||
return params
|
||||
|
||||
def _generate_auth_token(self) -> Tuple[str, str]:
|
||||
"""
|
||||
Generates the necessary authentication data to call the Subsonic API See the
|
||||
Authentication section of www.subsonic.org/pages/api.jsp for more information
|
||||
"""
|
||||
salt = "".join(random.choices(string.ascii_letters + string.digits, k=8))
|
||||
token = hashlib.md5(f"{self.password}{salt}".encode()).hexdigest()
|
||||
return (salt, token)
|
||||
|
||||
def _make_url(self, endpoint: str) -> str:
|
||||
return f"{self.hostname}/rest/{endpoint}.view"
|
||||
|
||||
|
@@ -21,6 +21,7 @@ def adapter_manager(tmp_path: Path):
|
||||
server_address="https://subsonic.example.com",
|
||||
username="test",
|
||||
verify_cert=True,
|
||||
salt_auth=False,
|
||||
)
|
||||
subsonic_config_store.set_secret("password", "testpass")
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@@ -21,11 +22,31 @@ def adapter(tmp_path: Path):
|
||||
server_address="https://subsonic.example.com",
|
||||
username="test",
|
||||
verify_cert=True,
|
||||
salt_auth=False,
|
||||
)
|
||||
config.set_secret("password", "testpass")
|
||||
|
||||
adapter = SubsonicAdapter(config, tmp_path)
|
||||
adapter._is_mock = True
|
||||
|
||||
yield adapter
|
||||
adapter.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def salt_auth_adapter(tmp_path: Path):
|
||||
ConfigurationStore.MOCK = True
|
||||
config = ConfigurationStore(
|
||||
server_address="https://subsonic.example.com",
|
||||
username="test",
|
||||
verify_cert=True,
|
||||
salt_auth=True,
|
||||
)
|
||||
config.set_secret("password", "testpass")
|
||||
|
||||
adapter = SubsonicAdapter(config, tmp_path)
|
||||
adapter._is_mock = True
|
||||
|
||||
yield adapter
|
||||
adapter.shutdown()
|
||||
|
||||
@@ -79,7 +100,7 @@ def test_config_form():
|
||||
SubsonicAdapter.get_configuration_form(config_store)
|
||||
|
||||
|
||||
def test_request_making_methods(adapter: SubsonicAdapter):
|
||||
def test_plain_auth_logic(adapter: SubsonicAdapter):
|
||||
expected = {
|
||||
"u": "test",
|
||||
"p": "testpass",
|
||||
@@ -89,6 +110,48 @@ def test_request_making_methods(adapter: SubsonicAdapter):
|
||||
}
|
||||
assert sorted(expected.items()) == sorted(adapter._get_params().items())
|
||||
|
||||
|
||||
def test_salt_auth_logic(salt_auth_adapter: SubsonicAdapter):
|
||||
expected = {
|
||||
"u": "test",
|
||||
"c": "Sublime Music",
|
||||
"f": "json",
|
||||
"v": "1.15.0",
|
||||
}
|
||||
|
||||
params = salt_auth_adapter._get_params()
|
||||
assert "p" not in params
|
||||
assert "s" in params
|
||||
salt = params["s"]
|
||||
assert "t" in params
|
||||
assert params["t"] == hashlib.md5(f"testpass{salt}".encode()).hexdigest()
|
||||
assert all(key in params and params[key] == expected[key] for key in expected)
|
||||
|
||||
|
||||
def test_migrate_configuration_populate_salt_auth():
|
||||
config = ConfigurationStore(
|
||||
server_address="https://subsonic.example.com",
|
||||
username="test",
|
||||
verify_cert=True,
|
||||
)
|
||||
SubsonicAdapter.migrate_configuration(config)
|
||||
assert "salt_auth" in config
|
||||
assert config["salt_auth"]
|
||||
|
||||
|
||||
def test_migrate_configuration_salt_auth_present():
|
||||
config = ConfigurationStore(
|
||||
server_address="https://subsonic.example.com",
|
||||
username="test",
|
||||
verify_cert=True,
|
||||
salt_auth=False,
|
||||
)
|
||||
SubsonicAdapter.migrate_configuration(config)
|
||||
assert "salt_auth" in config
|
||||
assert not config["salt_auth"]
|
||||
|
||||
|
||||
def test_make_url(adapter: SubsonicAdapter):
|
||||
assert adapter._make_url("foo") == "https://subsonic.example.com/rest/foo.view"
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user