From 84e2a0e23c4d9c013e839b331c649c0b875983dd Mon Sep 17 00:00:00 2001 From: andre Date: Sun, 12 Jul 2020 02:17:03 -0700 Subject: [PATCH] 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 --- sublime/adapters/subsonic/adapter.py | 34 +++++++++++++++- tests/adapter_tests/adapter_manager_tests.py | 1 + tests/adapter_tests/subsonic_adapter_tests.py | 39 ++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/sublime/adapters/subsonic/adapter.py b/sublime/adapters/subsonic/adapter.py index bfd5ed8..cdb1126 100644 --- a/sublime/adapters/subsonic/adapter.py +++ b/sublime/adapters/subsonic/adapter.py @@ -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" diff --git a/tests/adapter_tests/adapter_manager_tests.py b/tests/adapter_tests/adapter_manager_tests.py index afc6f35..355124d 100644 --- a/tests/adapter_tests/adapter_manager_tests.py +++ b/tests/adapter_tests/adapter_manager_tests.py @@ -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") diff --git a/tests/adapter_tests/subsonic_adapter_tests.py b/tests/adapter_tests/subsonic_adapter_tests.py index 989e140..df48009 100644 --- a/tests/adapter_tests/subsonic_adapter_tests.py +++ b/tests/adapter_tests/subsonic_adapter_tests.py @@ -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"