Skip to content

Commit 65e606b

Browse files
authored
fix: update Tumblr email validation module (#622)
* 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 * style: fix ruff linting issues in tumblr module * fix: add type annotation and correct exception import for curl_cffi * 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 * style: fix ruff linting issues - use specific exception
1 parent 58afd76 commit 65e606b

1 file changed

Lines changed: 96 additions & 51 deletions

File tree

Lines changed: 96 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,140 @@
1-
import re
2-
import urllib.request
3-
import urllib.error
4-
import json
51
import asyncio
2+
import re
3+
4+
from curl_cffi import requests
5+
66
from user_scanner.core.result import Result
77

88
# The public web app's bearer token, embedded in the homepage HTML.
99
API_TOKEN_RE = re.compile(r'"API_TOKEN":"([^"]+)"')
1010
VALIDATE_URL = "https://www.tumblr.com/api/v2/register/account/validate"
1111

12-
# response codes returned by the account-validate endpoint. A deliberately
13-
# short password means a free email always trips PASSWORD_TOO_SHORT (so no
14-
# account is created), while a taken email trips USER_EXISTS first regardless.
12+
# response codes returned by the account-validate endpoint.
1513
USER_EXISTS = 2
1614
PASSWORD_TOO_SHORT = 1030
1715

1816

1917
def _check_sync(email: str) -> Result:
2018
show_url = "https://tumblr.com"
19+
20+
# Use curl_cffi to impersonate a real Chrome browser
21+
session: requests.Session = requests.Session(impersonate="chrome131", timeout=15.0)
22+
2123
headers = {
22-
'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",
23-
'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
24-
'Accept-Language': "en-US,en;q=0.9",
24+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
25+
'Accept-Language': 'en-US,en;q=0.9',
26+
'Accept-Encoding': 'gzip, deflate, br',
27+
'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
28+
'Sec-Ch-Ua-Mobile': '?0',
29+
'Sec-Ch-Ua-Platform': '"Windows"',
30+
'Sec-Fetch-Dest': 'document',
31+
'Sec-Fetch-Mode': 'navigate',
32+
'Sec-Fetch-Site': 'none',
33+
'Sec-Fetch-User': '?1',
34+
'Upgrade-Insecure-Requests': '1',
35+
'Connection': 'keep-alive',
2536
}
37+
2638
try:
27-
# 1. Get Home Page
28-
req = urllib.request.Request("https://www.tumblr.com/", headers=headers)
29-
with urllib.request.urlopen(req, timeout=15.0) as response:
30-
html = response.read().decode("utf-8")
31-
39+
# 1. Get Home Page to extract API token
40+
response = session.get("https://www.tumblr.com/", headers=headers)
41+
html = response.text
42+
3243
token_match = API_TOKEN_RE.search(html)
3344
if not token_match:
3445
return Result.error("Token extraction failed, report it via GitHub issues")
3546
token = token_match.group(1)
3647

3748
# 2. Get Radar to extract CSRF
38-
req2 = urllib.request.Request("https://www.tumblr.com/api/v2/radar", headers={
39-
**headers,
40-
'Authorization': f"Bearer {token}"
41-
})
42-
with urllib.request.urlopen(req2, timeout=15.0) as response2:
43-
csrf = response2.headers.get("X-Csrf")
49+
radar_headers = {
50+
'Accept': 'application/json',
51+
'Accept-Language': 'en-US,en;q=0.9',
52+
'Accept-Encoding': 'gzip, deflate, br',
53+
'Authorization': f"Bearer {token}",
54+
'Origin': 'https://www.tumblr.com',
55+
'Referer': 'https://www.tumblr.com/',
56+
'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
57+
'Sec-Ch-Ua-Mobile': '?0',
58+
'Sec-Ch-Ua-Platform': '"Windows"',
59+
}
60+
response2 = session.get("https://www.tumblr.com/api/v2/radar", headers=radar_headers)
61+
csrf = response2.headers.get("X-Csrf")
62+
4463
if not csrf:
4564
return Result.error("CSRF extraction failed, report it via GitHub issues")
4665

4766
# 3. Post Account Validate
48-
req3 = urllib.request.Request(
67+
validate_headers = {
68+
'Accept': 'application/json',
69+
'Accept-Language': 'en-US,en;q=0.9',
70+
'Accept-Encoding': 'gzip, deflate, br',
71+
'Content-Type': 'application/json',
72+
'Authorization': f"Bearer {token}",
73+
'X-CSRF': csrf,
74+
'Origin': 'https://www.tumblr.com',
75+
'Referer': 'https://www.tumblr.com/register',
76+
'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
77+
'Sec-Ch-Ua-Mobile': '?0',
78+
'Sec-Ch-Ua-Platform': '"Windows"',
79+
}
80+
81+
payload = {
82+
'email': email,
83+
'password': "Password123!@#",
84+
'tumblelog': "osintuserprobe"
85+
}
86+
87+
response3 = session.post(
4988
VALIDATE_URL,
50-
headers={
51-
**headers,
52-
'Authorization': f"Bearer {token}",
53-
'X-CSRF': csrf,
54-
'Content-Type': "application/json",
55-
'Accept': "application/json",
56-
'Origin': "https://www.tumblr.com",
57-
'Referer': "https://www.tumblr.com/register",
58-
},
59-
data=json.dumps({'email': email, 'password': "x", 'tumblelog': "osintuserprobe"}).encode("utf-8"),
60-
method="POST"
89+
headers=validate_headers,
90+
json=payload
6191
)
62-
63-
try:
64-
with urllib.request.urlopen(req3, timeout=15.0) as response3:
65-
res_body = response3.read().decode("utf-8")
66-
except urllib.error.HTTPError as e:
67-
if e.code == 400:
68-
res_body = e.read().decode("utf-8")
69-
else:
70-
return Result.error(f"Unexpected HTTP status: {e.code}")
71-
72-
data = json.loads(res_body).get("response")
92+
93+
if response3.status_code == 400:
94+
# Check if it's a "User already exists" response
95+
try:
96+
response_data = response3.json()
97+
if "response" in response_data:
98+
data = response_data.get("response")
99+
code = data.get("code")
100+
error_msg = str(data.get("error", "")).lower()
101+
if code == 2 and "user already exists" in error_msg:
102+
return Result.taken(url=show_url)
103+
except (ValueError, KeyError):
104+
pass
105+
return Result.error(f"Unexpected HTTP status: {response3.status_code}")
106+
elif response3.status_code != 200:
107+
return Result.error(f"Unexpected HTTP status: {response3.status_code}")
108+
109+
response_data = response3.json()
110+
if "response" in response_data:
111+
data = response_data.get("response")
112+
else:
113+
data = response_data
114+
115+
# If the response is an empty list, it means the email is available
116+
if not data or data == []:
117+
return Result.available(url=show_url)
118+
73119
if not isinstance(data, dict):
74-
return Result.error("Invalid API response format, response is not a dict")
75-
120+
return Result.error(f"Invalid API response format: {data}")
121+
76122
code = data.get("code")
77123
error_msg = str(data.get("error", "")).lower()
78-
79-
# Check both response code and the description message to prevent false positives if the structure changes
124+
80125
if code == USER_EXISTS and "user already exists" in error_msg:
81126
return Result.taken(url=show_url)
82127
elif code == PASSWORD_TOO_SHORT and "password" in error_msg:
83128
return Result.available(url=show_url)
84129
else:
85130
return Result.error(f"Unexpected response (code: {code}, error: {error_msg}), report it via GitHub issues")
86131

87-
except Exception as e:
132+
except (requests.exceptions.RequestException, ValueError, KeyError, TypeError) as e:
88133
return Result.error(f"unexpected exception: {e}")
89134

90135

91136
async def validate_tumblr(email: str) -> Result:
92137
try:
93138
return await asyncio.to_thread(_check_sync, email)
94-
except Exception as e:
95-
return Result.error(f"unexpected exception: {e}")
139+
except (requests.exceptions.RequestException, ValueError, TypeError) as e:
140+
return Result.error(f"unexpected exception: {e}")

0 commit comments

Comments
 (0)