From 588a421443b0cc15d91bda986f6d71902b6078d0 Mon Sep 17 00:00:00 2001 From: Azaucifer <47237500+Azaucifer@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:40:38 +0530 Subject: [PATCH 1/5] fix: update Tumblr email validation module - Replace urllib with curl_cffi to bypass bot detection - Update User-Agent to Chrome 131 - Add modern browser headers (Sec-*) - Increase password strength to meet API requirements - Handle empty list response for available emails - Fixes 403 Forbidden error --- user_scanner/email_scan/social/tumblr.py | 126 ++++++++++++++--------- 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/user_scanner/email_scan/social/tumblr.py b/user_scanner/email_scan/social/tumblr.py index ae7685cb7..9741c3d2b 100644 --- a/user_scanner/email_scan/social/tumblr.py +++ b/user_scanner/email_scan/social/tumblr.py @@ -1,82 +1,114 @@ import re -import urllib.request -import urllib.error import json import asyncio from user_scanner.core.result import Result +from curl_cffi import requests # The public web app's bearer token, embedded in the homepage HTML. API_TOKEN_RE = re.compile(r'"API_TOKEN":"([^"]+)"') VALIDATE_URL = "https://www.tumblr.com/api/v2/register/account/validate" -# response codes returned by the account-validate endpoint. A deliberately -# short password means a free email always trips PASSWORD_TOO_SHORT (so no -# account is created), while a taken email trips USER_EXISTS first regardless. +# response codes returned by the account-validate endpoint. USER_EXISTS = 2 PASSWORD_TOO_SHORT = 1030 def _check_sync(email: str) -> Result: show_url = "https://tumblr.com" + + # Use curl_cffi to impersonate a real Chrome browser + session = requests.Session(impersonate="chrome131", timeout=15.0) + headers = { - 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - 'Accept-Language': "en-US,en;q=0.9", + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'Connection': 'keep-alive', } + try: - # 1. Get Home Page - req = urllib.request.Request("https://www.tumblr.com/", headers=headers) - with urllib.request.urlopen(req, timeout=15.0) as response: - html = response.read().decode("utf-8") - + # 1. Get Home Page to extract API token + response = session.get("https://www.tumblr.com/", headers=headers) + html = response.text + token_match = API_TOKEN_RE.search(html) if not token_match: return Result.error("Token extraction failed, report it via GitHub issues") token = token_match.group(1) # 2. Get Radar to extract CSRF - req2 = urllib.request.Request("https://www.tumblr.com/api/v2/radar", headers={ - **headers, - 'Authorization': f"Bearer {token}" - }) - with urllib.request.urlopen(req2, timeout=15.0) as response2: - csrf = response2.headers.get("X-Csrf") + radar_headers = { + 'Accept': 'application/json', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Authorization': f"Bearer {token}", + 'Origin': 'https://www.tumblr.com', + 'Referer': 'https://www.tumblr.com/', + 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + } + response2 = session.get("https://www.tumblr.com/api/v2/radar", headers=radar_headers) + csrf = response2.headers.get("X-Csrf") + if not csrf: return Result.error("CSRF extraction failed, report it via GitHub issues") # 3. Post Account Validate - req3 = urllib.request.Request( + validate_headers = { + 'Accept': 'application/json', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Content-Type': 'application/json', + 'Authorization': f"Bearer {token}", + 'X-CSRF': csrf, + 'Origin': 'https://www.tumblr.com', + 'Referer': 'https://www.tumblr.com/register', + 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + } + + payload = { + 'email': email, + 'password': "Password123!@#", + 'tumblelog': "osintuserprobe" + } + + response3 = session.post( VALIDATE_URL, - headers={ - **headers, - 'Authorization': f"Bearer {token}", - 'X-CSRF': csrf, - 'Content-Type': "application/json", - 'Accept': "application/json", - 'Origin': "https://www.tumblr.com", - 'Referer': "https://www.tumblr.com/register", - }, - data=json.dumps({'email': email, 'password': "x", 'tumblelog': "osintuserprobe"}).encode("utf-8"), - method="POST" + headers=validate_headers, + json=payload ) - - try: - with urllib.request.urlopen(req3, timeout=15.0) as response3: - res_body = response3.read().decode("utf-8") - except urllib.error.HTTPError as e: - if e.code == 400: - res_body = e.read().decode("utf-8") - else: - return Result.error(f"Unexpected HTTP status: {e.code}") - - data = json.loads(res_body).get("response") + + if response3.status_code != 200: + return Result.error(f"Unexpected HTTP status: {response3.status_code}") + + response_data = response3.json() + # If the response is directly the object (not wrapped in "response") + if "response" in response_data: + data = response_data.get("response") + else: + data = response_data + + # If the response is an empty list, it means the email is available + if not data or data == []: + return Result.available(url=show_url) + if not isinstance(data, dict): - return Result.error("Invalid API response format, response is not a dict") - + return Result.error(f"Invalid API response format: {data}") + code = data.get("code") error_msg = str(data.get("error", "")).lower() - - # Check both response code and the description message to prevent false positives if the structure changes + if code == USER_EXISTS and "user already exists" in error_msg: return Result.taken(url=show_url) elif code == PASSWORD_TOO_SHORT and "password" in error_msg: @@ -92,4 +124,4 @@ async def validate_tumblr(email: str) -> Result: try: return await asyncio.to_thread(_check_sync, email) except Exception as e: - return Result.error(f"unexpected exception: {e}") + return Result.error(f"unexpected exception: {e}") \ No newline at end of file From 6facad49ae985e0f8f7f0de6a5d5e8b625bb806d Mon Sep 17 00:00:00 2001 From: Azaucifer <47237500+Azaucifer@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:54:24 +0530 Subject: [PATCH 2/5] style: fix ruff linting issues in tumblr module --- user_scanner/email_scan/social/tumblr.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/user_scanner/email_scan/social/tumblr.py b/user_scanner/email_scan/social/tumblr.py index 9741c3d2b..16db3ba7d 100644 --- a/user_scanner/email_scan/social/tumblr.py +++ b/user_scanner/email_scan/social/tumblr.py @@ -1,9 +1,10 @@ -import re -import json import asyncio -from user_scanner.core.result import Result +import re + from curl_cffi import requests +from user_scanner.core.result import Result + # The public web app's bearer token, embedded in the homepage HTML. API_TOKEN_RE = re.compile(r'"API_TOKEN":"([^"]+)"') VALIDATE_URL = "https://www.tumblr.com/api/v2/register/account/validate" @@ -116,12 +117,12 @@ def _check_sync(email: str) -> Result: else: return Result.error(f"Unexpected response (code: {code}, error: {error_msg}), report it via GitHub issues") - except Exception as e: + except (requests.RequestException, ValueError, KeyError, TypeError) as e: return Result.error(f"unexpected exception: {e}") async def validate_tumblr(email: str) -> Result: try: return await asyncio.to_thread(_check_sync, email) - except Exception as e: + except (requests.RequestException, ValueError, TypeError) as e: return Result.error(f"unexpected exception: {e}") \ No newline at end of file From 53764d0833decf1b246c517d62c300752efd27c9 Mon Sep 17 00:00:00 2001 From: Azaucifer <47237500+Azaucifer@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:00:37 +0530 Subject: [PATCH 3/5] fix: add type annotation and correct exception import for curl_cffi --- user_scanner/email_scan/social/tumblr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_scanner/email_scan/social/tumblr.py b/user_scanner/email_scan/social/tumblr.py index 16db3ba7d..06bb42b90 100644 --- a/user_scanner/email_scan/social/tumblr.py +++ b/user_scanner/email_scan/social/tumblr.py @@ -18,7 +18,7 @@ def _check_sync(email: str) -> Result: show_url = "https://tumblr.com" # Use curl_cffi to impersonate a real Chrome browser - session = requests.Session(impersonate="chrome131", timeout=15.0) + session: requests.Session = requests.Session(impersonate="chrome131", timeout=15.0) headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', @@ -117,12 +117,12 @@ def _check_sync(email: str) -> Result: else: return Result.error(f"Unexpected response (code: {code}, error: {error_msg}), report it via GitHub issues") - except (requests.RequestException, ValueError, KeyError, TypeError) as e: + except (requests.exceptions.RequestException, ValueError, KeyError, TypeError) as e: return Result.error(f"unexpected exception: {e}") async def validate_tumblr(email: str) -> Result: try: return await asyncio.to_thread(_check_sync, email) - except (requests.RequestException, ValueError, TypeError) as e: + except (requests.exceptions.RequestException, ValueError, TypeError) as e: return Result.error(f"unexpected exception: {e}") \ No newline at end of file From 91f1fb0215aa6dcc586e0c89445d49329d48f75e Mon Sep 17 00:00:00 2001 From: Azaucifer <47237500+Azaucifer@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:57:29 +0530 Subject: [PATCH 4/5] fix: handle 400 response for registered emails in Tumblr module - Add handling for 'User already exists' response (code: 2) - Previously returned error for registered emails - Now correctly returns Result.taken() - Fixes the issue identified by maintainer --- user_scanner/email_scan/social/tumblr.py | 142 +++++++++++++++-------- 1 file changed, 93 insertions(+), 49 deletions(-) diff --git a/user_scanner/email_scan/social/tumblr.py b/user_scanner/email_scan/social/tumblr.py index ae7685cb7..1b534cf14 100644 --- a/user_scanner/email_scan/social/tumblr.py +++ b/user_scanner/email_scan/social/tumblr.py @@ -1,82 +1,126 @@ import re -import urllib.request -import urllib.error import json import asyncio from user_scanner.core.result import Result +from curl_cffi import requests # The public web app's bearer token, embedded in the homepage HTML. API_TOKEN_RE = re.compile(r'"API_TOKEN":"([^"]+)"') VALIDATE_URL = "https://www.tumblr.com/api/v2/register/account/validate" -# response codes returned by the account-validate endpoint. A deliberately -# short password means a free email always trips PASSWORD_TOO_SHORT (so no -# account is created), while a taken email trips USER_EXISTS first regardless. +# response codes returned by the account-validate endpoint. USER_EXISTS = 2 PASSWORD_TOO_SHORT = 1030 def _check_sync(email: str) -> Result: show_url = "https://tumblr.com" + + # Use curl_cffi to impersonate a real Chrome browser + session = requests.Session(impersonate="chrome131", timeout=15.0) + headers = { - 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - 'Accept-Language': "en-US,en;q=0.9", + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'Connection': 'keep-alive', } + try: - # 1. Get Home Page - req = urllib.request.Request("https://www.tumblr.com/", headers=headers) - with urllib.request.urlopen(req, timeout=15.0) as response: - html = response.read().decode("utf-8") - + # 1. Get Home Page to extract API token + response = session.get("https://www.tumblr.com/", headers=headers) + html = response.text + token_match = API_TOKEN_RE.search(html) if not token_match: return Result.error("Token extraction failed, report it via GitHub issues") token = token_match.group(1) # 2. Get Radar to extract CSRF - req2 = urllib.request.Request("https://www.tumblr.com/api/v2/radar", headers={ - **headers, - 'Authorization': f"Bearer {token}" - }) - with urllib.request.urlopen(req2, timeout=15.0) as response2: - csrf = response2.headers.get("X-Csrf") + radar_headers = { + 'Accept': 'application/json', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Authorization': f"Bearer {token}", + 'Origin': 'https://www.tumblr.com', + 'Referer': 'https://www.tumblr.com/', + 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + } + response2 = session.get("https://www.tumblr.com/api/v2/radar", headers=radar_headers) + csrf = response2.headers.get("X-Csrf") + if not csrf: return Result.error("CSRF extraction failed, report it via GitHub issues") # 3. Post Account Validate - req3 = urllib.request.Request( + validate_headers = { + 'Accept': 'application/json', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Content-Type': 'application/json', + 'Authorization': f"Bearer {token}", + 'X-CSRF': csrf, + 'Origin': 'https://www.tumblr.com', + 'Referer': 'https://www.tumblr.com/register', + 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + } + + payload = { + 'email': email, + 'password': "Password123!@#", + 'tumblelog': "osintuserprobe" + } + + response3 = session.post( VALIDATE_URL, - headers={ - **headers, - 'Authorization': f"Bearer {token}", - 'X-CSRF': csrf, - 'Content-Type': "application/json", - 'Accept': "application/json", - 'Origin': "https://www.tumblr.com", - 'Referer': "https://www.tumblr.com/register", - }, - data=json.dumps({'email': email, 'password': "x", 'tumblelog': "osintuserprobe"}).encode("utf-8"), - method="POST" + headers=validate_headers, + json=payload ) - - try: - with urllib.request.urlopen(req3, timeout=15.0) as response3: - res_body = response3.read().decode("utf-8") - except urllib.error.HTTPError as e: - if e.code == 400: - res_body = e.read().decode("utf-8") - else: - return Result.error(f"Unexpected HTTP status: {e.code}") - - data = json.loads(res_body).get("response") + + if response3.status_code == 400: + # Check if it's a "User already exists" response + try: + response_data = response3.json() + if "response" in response_data: + data = response_data.get("response") + code = data.get("code") + error_msg = str(data.get("error", "")).lower() + if code == 2 and "user already exists" in error_msg: + return Result.taken(url=show_url) + except: + pass + return Result.error(f"Unexpected HTTP status: {response3.status_code}") + elif response3.status_code != 200: + return Result.error(f"Unexpected HTTP status: {response3.status_code}") + + response_data = response3.json() + if "response" in response_data: + data = response_data.get("response") + else: + data = response_data + + # If the response is an empty list, it means the email is available + if not data or data == []: + return Result.available(url=show_url) + if not isinstance(data, dict): - return Result.error("Invalid API response format, response is not a dict") - + return Result.error(f"Invalid API response format: {data}") + code = data.get("code") error_msg = str(data.get("error", "")).lower() - - # Check both response code and the description message to prevent false positives if the structure changes + if code == USER_EXISTS and "user already exists" in error_msg: return Result.taken(url=show_url) elif code == PASSWORD_TOO_SHORT and "password" in error_msg: @@ -84,12 +128,12 @@ def _check_sync(email: str) -> Result: else: return Result.error(f"Unexpected response (code: {code}, error: {error_msg}), report it via GitHub issues") - except Exception as e: + except (requests.exceptions.RequestException, ValueError, KeyError, TypeError) as e: return Result.error(f"unexpected exception: {e}") async def validate_tumblr(email: str) -> Result: try: return await asyncio.to_thread(_check_sync, email) - except Exception as e: - return Result.error(f"unexpected exception: {e}") + except (requests.exceptions.RequestException, ValueError, TypeError) as e: + return Result.error(f"unexpected exception: {e}") \ No newline at end of file From 34c3ef9e3629f4833b708d597d09cce375e23def Mon Sep 17 00:00:00 2001 From: Azaucifer <47237500+Azaucifer@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:08:51 +0530 Subject: [PATCH 5/5] style: fix ruff linting issues - use specific exception --- user_scanner/email_scan/social/tumblr.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/user_scanner/email_scan/social/tumblr.py b/user_scanner/email_scan/social/tumblr.py index 6e661cbfd..5cb5cf92b 100644 --- a/user_scanner/email_scan/social/tumblr.py +++ b/user_scanner/email_scan/social/tumblr.py @@ -1,9 +1,10 @@ -import re -import json import asyncio -from user_scanner.core.result import Result +import re + from curl_cffi import requests +from user_scanner.core.result import Result + # The public web app's bearer token, embedded in the homepage HTML. API_TOKEN_RE = re.compile(r'"API_TOKEN":"([^"]+)"') VALIDATE_URL = "https://www.tumblr.com/api/v2/register/account/validate" @@ -99,7 +100,7 @@ def _check_sync(email: str) -> Result: error_msg = str(data.get("error", "")).lower() if code == 2 and "user already exists" in error_msg: return Result.taken(url=show_url) - except: + except (ValueError, KeyError): pass return Result.error(f"Unexpected HTTP status: {response3.status_code}") elif response3.status_code != 200: