mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-03-19 19:46:55 +08:00
fix: 绿联接口支持可配置SSL证书校验
This commit is contained in:
@@ -48,6 +48,7 @@ class Api:
|
|||||||
"_language",
|
"_language",
|
||||||
"_ug_agent",
|
"_ug_agent",
|
||||||
"_timeout",
|
"_timeout",
|
||||||
|
"_verify_ssl",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -57,6 +58,7 @@ class Api:
|
|||||||
language: str = "zh-CN",
|
language: str = "zh-CN",
|
||||||
ug_agent: str = "PC/WEB",
|
ug_agent: str = "PC/WEB",
|
||||||
timeout: int = 20,
|
timeout: int = 20,
|
||||||
|
verify_ssl: bool = True,
|
||||||
):
|
):
|
||||||
self._host = self._normalize_base_url(host)
|
self._host = self._normalize_base_url(host)
|
||||||
self._session = Session()
|
self._session = Session()
|
||||||
@@ -73,6 +75,8 @@ class Api:
|
|||||||
self._language = language
|
self._language = language
|
||||||
self._ug_agent = ug_agent
|
self._ug_agent = ug_agent
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
# 是否校验证书,默认开启;仅在用户明确配置时才应关闭。
|
||||||
|
self._verify_ssl = bool(verify_ssl)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self) -> str:
|
def host(self) -> str:
|
||||||
@@ -167,7 +171,7 @@ class Api:
|
|||||||
params=params,
|
params=params,
|
||||||
json=json_data,
|
json=json_data,
|
||||||
timeout=self._timeout,
|
timeout=self._timeout,
|
||||||
verify=False,
|
verify=self._verify_ssl,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
resp = self._session.get(
|
resp = self._session.get(
|
||||||
@@ -175,7 +179,7 @@ class Api:
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
params=params,
|
params=params,
|
||||||
timeout=self._timeout,
|
timeout=self._timeout,
|
||||||
verify=False,
|
verify=self._verify_ssl,
|
||||||
)
|
)
|
||||||
return resp.json()
|
return resp.json()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -219,7 +223,7 @@ class Api:
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
json={"username": username},
|
json={"username": username},
|
||||||
timeout=self._timeout,
|
timeout=self._timeout,
|
||||||
verify=False,
|
verify=self._verify_ssl,
|
||||||
)
|
)
|
||||||
check_json = check_resp.json()
|
check_json = check_resp.json()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -378,7 +382,7 @@ class Api:
|
|||||||
headers=req.headers,
|
headers=req.headers,
|
||||||
params=req.params,
|
params=req.params,
|
||||||
timeout=self._timeout,
|
timeout=self._timeout,
|
||||||
verify=False,
|
verify=self._verify_ssl,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Ugreen:
|
|||||||
_library_paths: dict[str, str] = {}
|
_library_paths: dict[str, str] = {}
|
||||||
_sync_libraries: List[str] = []
|
_sync_libraries: List[str] = []
|
||||||
_scan_type: int = 2
|
_scan_type: int = 2
|
||||||
|
_verify_ssl: bool = True
|
||||||
|
|
||||||
_api: Optional[Api] = None
|
_api: Optional[Api] = None
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ class Ugreen:
|
|||||||
sync_libraries: Optional[list] = None,
|
sync_libraries: Optional[list] = None,
|
||||||
scan_mode: Optional[Union[str, int]] = None,
|
scan_mode: Optional[Union[str, int]] = None,
|
||||||
scan_type: Optional[Union[str, int]] = None,
|
scan_type: Optional[Union[str, int]] = None,
|
||||||
|
verify_ssl: Optional[Union[bool, str, int]] = True,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if not host or not username or not password:
|
if not host or not username or not password:
|
||||||
@@ -51,6 +53,8 @@ class Ugreen:
|
|||||||
# 绿联媒体库扫描模式:
|
# 绿联媒体库扫描模式:
|
||||||
# 1 新添加和修改、2 补充缺失、3 覆盖扫描
|
# 1 新添加和修改、2 补充缺失、3 覆盖扫描
|
||||||
self._scan_type = self.__resolve_scan_type(scan_mode=scan_mode, scan_type=scan_type)
|
self._scan_type = self.__resolve_scan_type(scan_mode=scan_mode, scan_type=scan_type)
|
||||||
|
# HTTPS 证书校验开关:默认开启,仅兼容自签证书等场景下可关闭。
|
||||||
|
self._verify_ssl = self.__resolve_verify_ssl(verify_ssl)
|
||||||
|
|
||||||
if play_host:
|
if play_host:
|
||||||
self._playhost = UrlUtils.standardize_base_url(play_host).rstrip("/")
|
self._playhost = UrlUtils.standardize_base_url(play_host).rstrip("/")
|
||||||
@@ -144,7 +148,7 @@ class Ugreen:
|
|||||||
self.__remove_persisted_session()
|
self.__remove_persisted_session()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
api = Api(host=self._host)
|
api = Api(host=self._host, verify_ssl=self._verify_ssl)
|
||||||
if not api.import_session_state(cached):
|
if not api.import_session_state(cached):
|
||||||
api.close()
|
api.close()
|
||||||
self.__remove_persisted_session()
|
self.__remove_persisted_session()
|
||||||
@@ -174,7 +178,7 @@ class Ugreen:
|
|||||||
self.get_librarys()
|
self.get_librarys()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._api = Api(host=self._host)
|
self._api = Api(host=self._host, verify_ssl=self._verify_ssl)
|
||||||
if self._api.login(self._username, self._password) is None:
|
if self._api.login(self._username, self._password) is None:
|
||||||
self.__remove_persisted_session()
|
self.__remove_persisted_session()
|
||||||
return False
|
return False
|
||||||
@@ -454,6 +458,19 @@ class Ugreen:
|
|||||||
}
|
}
|
||||||
return mode_map.get(mode, 2)
|
return mode_map.get(mode, 2)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __resolve_verify_ssl(verify_ssl: Optional[Union[bool, str, int]]) -> bool:
|
||||||
|
if isinstance(verify_ssl, bool):
|
||||||
|
return verify_ssl
|
||||||
|
if verify_ssl is None:
|
||||||
|
return True
|
||||||
|
value = str(verify_ssl).strip().lower()
|
||||||
|
if value in {"1", "true", "yes", "on"}:
|
||||||
|
return True
|
||||||
|
if value in {"0", "false", "no", "off"}:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def __scan_library(self, library_id: str, scan_type: Optional[int] = None) -> bool:
|
def __scan_library(self, library_id: str, scan_type: Optional[int] = None) -> bool:
|
||||||
if not self._api:
|
if not self._api:
|
||||||
return False
|
return False
|
||||||
@@ -561,7 +578,7 @@ class Ugreen:
|
|||||||
if not username or not password or not self._host:
|
if not username or not password or not self._host:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
api = Api(self._host)
|
api = Api(self._host, verify_ssl=self._verify_ssl)
|
||||||
try:
|
try:
|
||||||
return api.login(username, password)
|
return api.login(username, password)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
113
tests/test_ugreen_api.py
Normal file
113
tests/test_ugreen_api.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from app.modules.ugreen.api import Api
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeResponse:
|
||||||
|
def __init__(self, payload: dict, headers: dict | None = None):
|
||||||
|
self._payload = payload
|
||||||
|
self.headers = headers or {}
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeSession:
|
||||||
|
def __init__(self, get_responses=None, post_responses=None):
|
||||||
|
self._get_responses = list(get_responses or [])
|
||||||
|
self._post_responses = list(post_responses or [])
|
||||||
|
self.calls: list[tuple[str, dict]] = []
|
||||||
|
self.cookies = SimpleNamespace(
|
||||||
|
get_dict=lambda: {},
|
||||||
|
update=lambda *_args, **_kwargs: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
if args:
|
||||||
|
kwargs = {"url": args[0], **kwargs}
|
||||||
|
self.calls.append(("GET", kwargs))
|
||||||
|
return self._get_responses.pop(0) if self._get_responses else _FakeResponse({})
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
if args:
|
||||||
|
kwargs = {"url": args[0], **kwargs}
|
||||||
|
self.calls.append(("POST", kwargs))
|
||||||
|
return self._post_responses.pop(0) if self._post_responses else _FakeResponse({})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def close():
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeCrypto:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rsa_encrypt_long(raw: str) -> str:
|
||||||
|
return f"enc:{raw}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_encrypted_request(url: str, method: str = "GET", params=None, **kwargs):
|
||||||
|
return SimpleNamespace(url=url, headers={}, params=params or {}, json=None, aes_key="k")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decrypt_response(payload, aes_key):
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class UgreenApiVerifySslTest(unittest.TestCase):
|
||||||
|
def test_request_json_default_verify_ssl_true(self):
|
||||||
|
api = Api(host="https://example.com")
|
||||||
|
fake_session = _FakeSession(
|
||||||
|
get_responses=[_FakeResponse({"code": 200})],
|
||||||
|
post_responses=[_FakeResponse({"code": 200})],
|
||||||
|
)
|
||||||
|
api._session = fake_session
|
||||||
|
|
||||||
|
api._request_json(url="https://example.com/a", method="GET")
|
||||||
|
api._request_json(url="https://example.com/b", method="POST", json_data={"x": 1})
|
||||||
|
|
||||||
|
self.assertEqual(fake_session.calls[0][1].get("verify"), True)
|
||||||
|
self.assertEqual(fake_session.calls[1][1].get("verify"), True)
|
||||||
|
|
||||||
|
def test_login_logout_follow_verify_ssl_flag(self):
|
||||||
|
api = Api(host="https://example.com", verify_ssl=False)
|
||||||
|
fake_session = _FakeSession(
|
||||||
|
get_responses=[_FakeResponse({})],
|
||||||
|
post_responses=[
|
||||||
|
_FakeResponse({"code": 200, "msg": "ok", "data": {}}, headers={"x-rsa-token": "BEGIN TEST"}),
|
||||||
|
_FakeResponse(
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"token": "token-value",
|
||||||
|
"public_key": "BEGIN LOGIN KEY",
|
||||||
|
"static_token": "static-token",
|
||||||
|
"is_ugk": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
api._session = fake_session
|
||||||
|
|
||||||
|
with patch("app.modules.ugreen.api.UgreenCrypto", _FakeCrypto):
|
||||||
|
token = api.login("tester", "pwd")
|
||||||
|
self.assertEqual(token, "token-value")
|
||||||
|
api.logout()
|
||||||
|
|
||||||
|
self.assertEqual(len(fake_session.calls), 3)
|
||||||
|
self.assertEqual(fake_session.calls[0][0], "POST")
|
||||||
|
self.assertEqual(fake_session.calls[1][0], "POST")
|
||||||
|
self.assertEqual(fake_session.calls[2][0], "GET")
|
||||||
|
self.assertEqual(fake_session.calls[0][1].get("verify"), False)
|
||||||
|
self.assertEqual(fake_session.calls[1][1].get("verify"), False)
|
||||||
|
self.assertEqual(fake_session.calls[2][1].get("verify"), False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -121,6 +121,18 @@ class UgreenScanModeTest(unittest.TestCase):
|
|||||||
self.assertEqual(resolve(), 2)
|
self.assertEqual(resolve(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class UgreenVerifySslTest(unittest.TestCase):
|
||||||
|
def test_resolve_verify_ssl(self):
|
||||||
|
resolve = Ugreen._Ugreen__resolve_verify_ssl
|
||||||
|
self.assertEqual(resolve(True), True)
|
||||||
|
self.assertEqual(resolve(False), False)
|
||||||
|
self.assertEqual(resolve("true"), True)
|
||||||
|
self.assertEqual(resolve("1"), True)
|
||||||
|
self.assertEqual(resolve("false"), False)
|
||||||
|
self.assertEqual(resolve("0"), False)
|
||||||
|
self.assertEqual(resolve(None), True)
|
||||||
|
|
||||||
|
|
||||||
class UgreenStatisticTest(unittest.TestCase):
|
class UgreenStatisticTest(unittest.TestCase):
|
||||||
def test_get_medias_count_episode_is_none(self):
|
def test_get_medias_count_episode_is_none(self):
|
||||||
ugreen = Ugreen.__new__(Ugreen)
|
ugreen = Ugreen.__new__(Ugreen)
|
||||||
|
|||||||
Reference in New Issue
Block a user