mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-15 12:50:20 +08:00
fix(site): preserve cookies when redirecting to HTTPS
This commit is contained in:
@@ -68,6 +68,34 @@ def _redirect_destination(url: str, response: Any) -> tuple[Optional[str], Optio
|
||||
return destination, None
|
||||
|
||||
|
||||
def _redirect_keeps_credentials(source: str, destination: str) -> bool:
|
||||
"""
|
||||
判断站点重定向是否仍可携带原请求的登录凭据。
|
||||
|
||||
除同源跳转外,仅允许同主机从默认 HTTP 端口升级到默认 HTTPS 端口,
|
||||
避免站点强制 HTTPS 时丢失 Cookie;协议降级和其它来源变更均不保留凭据。
|
||||
|
||||
:param source: 当前请求的绝对地址
|
||||
:param destination: 重定向目标的绝对地址
|
||||
:return: 两地址同源或符合默认端口的安全升级条件时返回 True
|
||||
"""
|
||||
if UrlUtils.same_origin(source, destination):
|
||||
return True
|
||||
source_url = UrlUtils.normalize_http_url(source)
|
||||
destination_url = UrlUtils.normalize_http_url(destination)
|
||||
if source_url is None or destination_url is None:
|
||||
return False
|
||||
origin, target = urlsplit(source_url), urlsplit(destination_url)
|
||||
return (
|
||||
origin.scheme == "http"
|
||||
and target.scheme == "https"
|
||||
and origin.hostname == target.hostname
|
||||
and origin.port in (None, 80)
|
||||
and target.port in (None, 443)
|
||||
and not (origin.username or origin.password or target.username or target.password)
|
||||
)
|
||||
|
||||
|
||||
class _RssHttpAdapter:
|
||||
"""
|
||||
用 RequestUtils 实现 RSS HTTP 窄端口。
|
||||
@@ -236,8 +264,9 @@ class _SiteHttpAdapter:
|
||||
"""
|
||||
打开响应并在所有返回或异常分支后关闭底层连接。
|
||||
|
||||
GET 请求逐跳跟随重定向,跨域跳转清除 Cookie 与认证头。重定向缺少目标、目标无效、出现环路
|
||||
或超过跳数上限时只记录来源地址的告警并提供 None,与请求失败的语义一致,
|
||||
GET 请求逐跳跟随重定向,同源或同主机默认端口的 HTTP 到 HTTPS 升级保留凭据,
|
||||
其它来源变更清除 Cookie 与认证头。重定向缺少目标、目标无效、出现环路或超过跳数上限时
|
||||
只记录来源地址的告警并提供 None,与请求失败的语义一致,
|
||||
由调用方按无法打开站点处理,不能以异常中断 CookieCloud 同步等批量流程。
|
||||
|
||||
:param method: HTTP 请求方法
|
||||
@@ -281,7 +310,7 @@ class _SiteHttpAdapter:
|
||||
if destination is None:
|
||||
failure = invalid_reason or failure
|
||||
break
|
||||
if not UrlUtils.same_origin(url, destination):
|
||||
if not _redirect_keeps_credentials(url, destination):
|
||||
cookie = None
|
||||
request_headers = {
|
||||
key: value for key, value in request_headers.items()
|
||||
|
||||
@@ -480,6 +480,103 @@ def _scripted_request(monkeypatch, responses: list) -> list[dict]:
|
||||
return hops
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [301, 302, 303, 307, 308])
|
||||
@pytest.mark.parametrize("source,destination", [
|
||||
("http://hdwing.org/", "https://hdwing.org/"),
|
||||
("http://hdwing.org:80/", "https://hdwing.org:443/index.php"),
|
||||
("http://HDWING.org/index.php", "https://hdwing.org/"),
|
||||
("http://hdwing.org/", "http://hdwing.org/index.php"),
|
||||
("https://hdwing.org/", "https://hdwing.org/index.php"),
|
||||
])
|
||||
def test_site_adapter_keeps_credentials_on_same_origin_or_https_upgrade(
|
||||
monkeypatch, status: int, source: str, destination: str,
|
||||
) -> None:
|
||||
"""
|
||||
同源跳转与同主机默认端口的 HTTPS 升级应保留 Cookie 和认证头。
|
||||
"""
|
||||
final = _FakeResponse(200, text="ok")
|
||||
hops = _scripted_request(monkeypatch, [
|
||||
SimpleNamespace(status_code=status, headers={"Location": destination}), final,
|
||||
])
|
||||
headers = {"Cookie": "sid=header", "Authorization": "Bearer test", "Referer": source}
|
||||
|
||||
with site_composition._SiteHttpAdapter().open(
|
||||
method="GET", url=source, cookie="uid=1", headers=headers,
|
||||
) as response:
|
||||
assert response is final
|
||||
|
||||
assert [hop["url"] for hop in hops] == [source, destination]
|
||||
assert [hop["cookies"] for hop in hops] == ["uid=1", "uid=1"]
|
||||
assert [hop["headers"] for hop in hops] == [headers, headers]
|
||||
assert all(hop["allow_redirects"] is False for hop in hops)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source,destination", [
|
||||
("http://hdwing.org/", "https://other.test/"),
|
||||
("http://hdwing.org/", "https://www.hdwing.org/"),
|
||||
("http://hdwing.org:8080/", "https://hdwing.org/"),
|
||||
("http://hdwing.org/", "https://hdwing.org:8443/"),
|
||||
("http://hdwing.org:443/", "https://hdwing.org/"),
|
||||
("http://hdwing.org/", "https://hdwing.org:80/"),
|
||||
("https://hdwing.org/", "http://hdwing.org/"),
|
||||
("https://hdwing.org/", "https://hdwing.org:8443/"),
|
||||
])
|
||||
def test_site_adapter_strips_credentials_when_redirect_changes_trust(
|
||||
monkeypatch, source: str, destination: str,
|
||||
) -> None:
|
||||
"""
|
||||
跨主机、非默认端口升级与 HTTPS 降级仍应清除全部敏感请求头。
|
||||
"""
|
||||
hops = _scripted_request(monkeypatch, [_redirect(destination), _FakeResponse(200)])
|
||||
with site_composition._SiteHttpAdapter().open(
|
||||
method="GET", url=source, cookie="uid=1",
|
||||
headers={
|
||||
"cOoKiE": "sid=header", "aUtHoRiZaTiOn": "Bearer test",
|
||||
"Proxy-Authorization": "Basic test", "rEfErEr": source, "Accept": "text/html",
|
||||
},
|
||||
) as response:
|
||||
assert response.status_code == 200
|
||||
|
||||
assert [hop["cookies"] for hop in hops] == ["uid=1", ""]
|
||||
assert hops[1]["headers"] == {"Accept": "text/html"}
|
||||
|
||||
|
||||
def test_site_adapter_does_not_restore_credentials_after_downgrade_then_upgrade(monkeypatch) -> None:
|
||||
"""
|
||||
HTTPS 降级清除的凭据,在后续同主机升级回 HTTPS 时不能重新出现。
|
||||
"""
|
||||
hops = _scripted_request(monkeypatch, [
|
||||
_redirect("http://hdwing.org/next"),
|
||||
_redirect("https://hdwing.org/final"),
|
||||
_FakeResponse(200),
|
||||
])
|
||||
with site_composition._SiteHttpAdapter().open(
|
||||
method="GET", url="https://hdwing.org/", cookie="uid=1",
|
||||
headers={"Authorization": "Bearer test"},
|
||||
) as response:
|
||||
assert response.status_code == 200
|
||||
|
||||
assert [hop["cookies"] for hop in hops] == ["uid=1", "", ""]
|
||||
assert [hop["headers"] for hop in hops] == [{"Authorization": "Bearer test"}, {}, {}]
|
||||
|
||||
|
||||
def test_site_connection_keeps_cookie_when_hdwing_redirects_to_https(monkeypatch) -> None:
|
||||
"""
|
||||
HDWing 的 HTTP 入口升级到 HTTPS 后,连接测试应携带原 Cookie 并识别已登录页面。
|
||||
"""
|
||||
hops = _scripted_request(monkeypatch, [
|
||||
_redirect("https://hdwing.org/"),
|
||||
_FakeResponse(200, text='<html><body><a href="logout.php">退出</a></body></html>'),
|
||||
])
|
||||
_configure(site_composition._SiteHttpAdapter())
|
||||
site = SiteSnapshot(
|
||||
id=1, name="瓷器", domain="hdwing.org", url="http://hdwing.org/", cookie="uid=1",
|
||||
)
|
||||
|
||||
assert _chain()._SiteChain__test(site) == (True, "连接成功")
|
||||
assert [hop["cookies"] for hop in hops] == ["uid=1", "uid=1"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("responses,reason", [
|
||||
([_redirect("/dl/1/auth?X-Amz-Signature=sig")] * 2, "站点请求重定向出现环路"),
|
||||
([_redirect(None)], "站点请求重定向缺少目标地址"),
|
||||
|
||||
Reference in New Issue
Block a user