diff --git a/maigret/error_detection.py b/maigret/error_detection.py index d015495d..693570b5 100644 --- a/maigret/error_detection.py +++ b/maigret/error_detection.py @@ -40,11 +40,12 @@ def detect_error_page( return CheckError("Rate limited", "429 status code") if status_code == 999: # LinkedIn's "Request denied", served to blocked clients for existing - # and non-existing profiles alike, so it is not really a not-found. - # Classifying it as an error would flip LinkedIn to UNKNOWN for every - # blocked IP, which reads as "LinkedIn is broken" to users. Kept as a - # pass-through on purpose: the caller's checkType branch decides. - return None + # and non-existing profiles alike. Falling through to the checkType + # branch turned every profile into AVAILABLE, so a blocked client was + # told that williamhgates has no LinkedIn. "We could not check" is the + # honest answer here; the cost is that a blocked IP now sees an error + # instead of a confident wrong "Not found!". + return CheckError("Access denied", "999 status code") if status_code >= 500: return CheckError("Server", f"{status_code} status code") return None diff --git a/maigret/errors.py b/maigret/errors.py index 23861497..81e3f974 100644 --- a/maigret/errors.py +++ b/maigret/errors.py @@ -84,6 +84,12 @@ COMMON_ERRORS = { 'Captcha', 'Google rate-limit / captcha' ), 'id="gs_captcha_f"': CheckError('Captcha', 'Google rate-limit / captcha'), + # Google reCAPTCHA interstitial: answers HTTP 200 on every path, so on a + # status_code site every username reads as claimed (linkedin.com). Matches + # the challenge page itself, not a login form that merely embeds a widget. + 'google.com/recaptcha/challengepage/': CheckError( + 'Captcha', 'Google reCAPTCHA interstitial' + ), } PROXY_RECOMMENDATION = ( diff --git a/tests/test_checking.py b/tests/test_checking.py index 3272c475..950c87f5 100644 --- a/tests/test_checking.py +++ b/tests/test_checking.py @@ -149,9 +149,13 @@ def test_detect_error_page_403_ignored(): def test_detect_error_page_999_linkedin(): - # LinkedIn returns 999 on bot suspicion. Deliberately not an error: making - # it one turns LinkedIn UNKNOWN for every blocked IP. - assert detect_error_page("", 999, {}, ignore_403=False) is None + # LinkedIn returns 999 on bot suspicion, for real and made-up profiles + # alike. It has to be an error: passing it through made every profile + # look free to a blocked client. + err = detect_error_page("", 999, {}, ignore_403=False) + assert err is not None + assert err.type == "Access denied" + assert "999" in err.desc def test_detect_error_page_500(): diff --git a/tests/test_error_detection.py b/tests/test_error_detection.py index 310050ab..94278211 100644 --- a/tests/test_error_detection.py +++ b/tests/test_error_detection.py @@ -40,10 +40,6 @@ def test_http_429_is_rate_limit(): assert err.type == "Rate limited" -def test_ignore_linkedin_999_status(): - # 999 stays a pass-through on purpose, see detect_error_page. - assert detect_error_page("", 999, {}, ignore_403=False) is None - def test_pow_challenge_pages_are_bot_protection(): # Both serve a 2xx on every path, so without a marker every username # would read as claimed. @@ -59,3 +55,34 @@ def test_pow_challenge_pages_are_bot_protection(): assert anubis.type == "Bot protection" assert pow_js.type == "Bot protection" + + +def test_linkedin_999_is_an_error_not_an_absence(): + # LinkedIn serves 999 to blocked clients for existing and non-existing + # profiles alike. Passing it through to the status_code branch reported + # every profile as free, so a blocked user was told that a real account + # does not exist. + err = detect_error_page("", 999, {}, ignore_403=False) + assert err is not None + assert err.type == "Access denied" + + +def test_google_recaptcha_interstitial_is_a_captcha(): + # Answers 200 on every path, so without a marker every username reads as + # claimed on a status_code site. + err = detect_error_page( + '