Added subsonic server config for salt auth logic
Allows users to toggle between the plain password auth and the salted auth. Salted auth is turned off by default since its only supported after Subsonic API 1.13.0
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
|
||||
@@ -109,6 +111,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=False,
|
||||
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:
|
||||
@@ -210,6 +221,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"] if "salt_auth" in config else False
|
||||
|
||||
self.is_shutting_down = False
|
||||
|
||||
@@ -333,14 +345,32 @@ 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": "1.15.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))
|
||||
unhashed_token = "{}{}".format(self.password, salt)
|
||||
hashed_token = hashlib.md5(str.encode(unhashed_token)).hexdigest()
|
||||
return (salt, hashed_token)
|
||||
|
||||
def _make_url(self, endpoint: str) -> str:
|
||||
return f"{self.hostname}/rest/{endpoint}.view"
|
||||
|
||||
|
@@ -16,6 +16,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")
|
||||
|
||||
|
@@ -24,11 +24,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()
|
||||
|
||||
@@ -82,7 +102,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",
|
||||
@@ -92,6 +112,23 @@ 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
|
||||
assert "t" in params
|
||||
assert all(key in params and params[key] == expected[key] for key in expected)
|
||||
|
||||
|
||||
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