1- import re
2- import urllib .request
3- import urllib .error
4- import json
51import asyncio
2+ import re
3+
4+ from curl_cffi import requests
5+
66from user_scanner .core .result import Result
77
88# The public web app's bearer token, embedded in the homepage HTML.
99API_TOKEN_RE = re .compile (r'"API_TOKEN":"([^"]+)"' )
1010VALIDATE_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.
1513USER_EXISTS = 2
1614PASSWORD_TOO_SHORT = 1030
1715
1816
1917def _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
91136async 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