컨테이너를 로컬에 재현해 규명했다. '컨테이너가 문제'가 아니라 **UA 가 리눅스라고 말하는 것**이 원인이다. 네이버는 리눅스 데스크톱 Chrome 을 HTTP 405 로 거부한다. 실측(같은 이미지·같은 KR 프록시, 서로 다른 IP 3개씩): X11; Linux x86_64 0/3 통과 전부 405 + wtm_captcha (~50KB) Macintosh; Intel Mac 3/3 통과 전부 200 · 6건 · ~1.0MB 안드로이드·아이폰 모바일 0/2 418 '비정상적인 접근'(2.6KB, 회전 무효 하드차단) 프록시 없이 같은 집 IP 로도 호스트 통과 / 컨테이너 차단이 재현돼 IP·게이트웨이는 배제됐다. 맥에서 잘 되던 이유도 이걸로 설명된다. **405 가 열쇠였다** — JS 가 돌기 전에 HTTP 계층에서 거부당한다. 그래서 그동안 의심하던 WebGL·폰트·plugins 는 애초에 원인이 될 수 없었다(확인차 --enable-unsafe-swiftshader 로 WebGL 을 살려봤지만 405 그대로였다). 조치: - services/search/user_agent.py: 리눅스에서만 UA 플랫폼 토큰을 맥으로 치환. Chrome 버전은 `--version` 으로 실제 값을 읽어 유지한다 — 하드코딩하면 컨테이너 Chrome 업데이트 시 UA 와 엔진이 어긋나 그 불일치가 새 봇 신호가 된다. 조회 실패해도 크롤을 막지 않는다. - NaverShopAdapter.mac_ua_on_linux = True (쿠팡은 잘 통과하므로 기본 False 그대로 — 멀쩡한 걸 건드리지 않는다). 맥/윈도우에서는 자동 미적용. - 실제 어댑터로 컨테이너 검증: '생수' 40건, '스페셜티 원두 1kg' 40건 통과. 부수: - fingerprint.judge: WebGL 이 **아예 없는** 경우를 OK 로 흘려보내던 판정 버그 수정(컨테이너 재현 중 발견 — 소프트웨어 렌더링보다 더 튀는 값인데 침묵했다). UA 플랫폼 항목 추가. - docs/operations.md: '미해결' 절을 원인·수치·조치·확인법으로 교체. - fingerprint.py: JS 위장이 이 스택에서 불가능하다는 실측 기록 유지(재시도 방지). 테스트 8건 추가(리눅스에서만 보정·실제 버전 유지·조회 실패 폴백·네이버만 opt-in), 전체 270 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
71 lines
3.2 KiB
Python
71 lines
3.2 KiB
Python
"""UA 플랫폼 보정 테스트 — 네이버가 리눅스 데스크톱 Chrome 을 405 로 거부하는 문제 대응.
|
|
|
|
여기서 지키는 계약:
|
|
- 리눅스에서만 보정한다(맥·윈도우는 이미 통과 — 굳이 위장하면 나머지 지문과 어긋나 오히려 튄다)
|
|
- **버전은 실제 Chrome 값**을 쓴다(하드코딩하면 업데이트 시 불일치가 새 신호가 된다)
|
|
- 조회 실패해도 크롤을 막지 않는다
|
|
"""
|
|
|
|
from services.search import user_agent as ua_mod
|
|
from services.search.user_agent import mac_ua, needs_mac_ua, parse_major, resolve_ua
|
|
|
|
|
|
def test_only_linux_gets_the_override():
|
|
"""맥·윈도우를 위장하면 UA 와 실제 플랫폼 지문이 어긋나 오히려 봇 신호가 된다."""
|
|
assert needs_mac_ua("linux") is True
|
|
assert needs_mac_ua("darwin") is False
|
|
assert needs_mac_ua("win32") is False
|
|
|
|
|
|
def test_resolve_returns_none_off_linux():
|
|
assert resolve_ua("/opt/google/chrome/chrome", platform="darwin") is None
|
|
assert resolve_ua(None, platform="win32") is None
|
|
|
|
|
|
def test_resolve_on_linux_produces_a_mac_ua(monkeypatch):
|
|
monkeypatch.setattr(ua_mod, "chrome_major", lambda _exe: "150")
|
|
ua = resolve_ua("/opt/google/chrome/chrome", platform="linux")
|
|
assert "Macintosh; Intel Mac OS X" in ua
|
|
assert "Linux" not in ua and "X11" not in ua
|
|
|
|
|
|
def test_ua_keeps_the_real_chrome_major(monkeypatch):
|
|
"""UA 버전이 실제 엔진과 어긋나면 그 불일치가 새로운 지문이 된다."""
|
|
monkeypatch.setattr(ua_mod, "chrome_major", lambda _exe: "150")
|
|
assert "Chrome/150.0.0.0" in resolve_ua("/x", platform="linux")
|
|
monkeypatch.setattr(ua_mod, "chrome_major", lambda _exe: "151")
|
|
assert "Chrome/151.0.0.0" in resolve_ua("/x", platform="linux")
|
|
|
|
|
|
def test_ua_shape_matches_real_chrome():
|
|
"""형식이 어긋나면 그 자체로 탐지된다 — 실제 Chrome 의 reduced UA 형태를 지킨다."""
|
|
ua = mac_ua("150")
|
|
assert ua.startswith("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")
|
|
assert "AppleWebKit/537.36 (KHTML, like Gecko)" in ua
|
|
assert ua.endswith("Safari/537.36")
|
|
|
|
|
|
def test_parse_major_from_chrome_version_output():
|
|
assert parse_major("Google Chrome 150.0.7258.66 ") == "150"
|
|
assert parse_major("Chromium 138.0.7204.100") == "138"
|
|
assert parse_major("") is None
|
|
assert parse_major("no version here") is None
|
|
|
|
|
|
def test_version_lookup_failure_still_yields_a_valid_ua(monkeypatch):
|
|
"""--version 조회가 실패해도 크롤이 멈추면 안 된다 — 형식이 온전한 UA 를 낸다."""
|
|
monkeypatch.setattr(ua_mod.subprocess, "run", lambda *a, **k: (_ for _ in ()).throw(OSError("no chrome")))
|
|
ua_mod.chrome_major.cache_clear()
|
|
ua = resolve_ua("/nonexistent", platform="linux")
|
|
assert "Macintosh" in ua and "Chrome/" in ua
|
|
ua_mod.chrome_major.cache_clear()
|
|
|
|
|
|
def test_naver_opts_in_and_coupang_does_not():
|
|
"""UA 보정은 네이버만 켠다 — 쿠팡은 지금 잘 통과하므로 건드리지 않는다."""
|
|
from services.search.coupang.adapter import CoupangAdapter
|
|
from services.search.naver_shop.adapter import NaverShopAdapter
|
|
|
|
assert NaverShopAdapter.mac_ua_on_linux is True
|
|
assert CoupangAdapter.mac_ua_on_linux is False
|