XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

feat(deepinfra): wip custom cdp turnstile bypass

f7b00a69
kqlio67 <kqlio67@users.noreply.github.com>
提交于

代码差异

4 个文件 +415 -163
Deleted g4f/Provider/DeepInfra.py +0 -162
@@ -1,162 +0,0 @@
1 from __future__ import annotations
2
3 import time
4 import asyncio
5 import requests
6
7 from ..requests import get_nodriver_session
8 from ..errors import MissingRequirementsError
9 from .template import OpenaiTemplate
10
11 async def get_turnstile_token_async() -> str:
12 try:
13 import zendriver as zd
14 except ImportError:
15 return None
16
17 async with get_nodriver_session() as session:
18 # Generate a token on any model's page; it is valid for the entire domain
19 tab = await session.get('https://deepinfra.com/' + DeepInfra.default_model)
20
21 # Inject JS to block the original request
22 js_block_fetch = """
23 const origFetch = window.fetch;
24 window.fetch = async function(...args) {
25 let url = args[0];
26 if (typeof url === 'string' && url.includes('/chat/completions')) {
27 return new Response('{}', {status: 200});
28 }
29 return origFetch.apply(this, args);
30 };
31 """
32 await tab.evaluate(js_block_fetch)
33
34 # Initiate Turnstile
35 textarea = await tab.find('textarea', timeout=15)
36 if not textarea:
37 return ""
38
39 await textarea.send_keys('Test\n')
40
41 # Wait for the challenge to be solved
42 token = ""
43 for _ in range(40):
44 token = await tab.evaluate(
45 "(document.querySelector('[name=cf-turnstile-response]') || {value: ''}).value"
46 )
47 if token:
48 break
49 await asyncio.sleep(0.5)
50
51 return token
52
53 def get_turnstile_token() -> str:
54 """
55 Opens the DeepInfra page using DrissionPage to obtain a Turnstile token.
56 Raises MissingRequirementsError if DrissionPage is not installed.
57 """
58 try:
59 from DrissionPage import ChromiumPage, ChromiumOptions
60 except ImportError:
61 raise MissingRequirementsError('Install "DrissionPage" package to use DeepInfra without an API key | pip install DrissionPage')
62
63 co = ChromiumOptions()
64 # Hide the window off-screen
65 co.set_argument('--window-position=-2000,-2000')
66 co.set_argument('--window-size=800,600')
67 co.set_argument('--log-level=3')
68
69 page = ChromiumPage(co)
70
71 try:
72 # Generate a token on any model's page; it is valid for the entire domain
73 page.get('https://deepinfra.com/' + DeepInfra.default_model)
74
75 # Inject JS to block the original request
76 js_block_fetch = """
77 const origFetch = window.fetch;
78 window.fetch = async function(...args) {
79 let url = args[0];
80 if (typeof url === 'string' && url.includes('/chat/completions')) {
81 return new Response('{}', {status: 200});
82 }
83 return origFetch.apply(this, args);
84 };
85 """
86 page.run_js(js_block_fetch)
87
88 # Initiate Turnstile
89 textarea = page.ele('tag:textarea', timeout=15)
90 if not textarea:
91 return ""
92
93 textarea.input('Test')
94 textarea.input('\n')
95
96 # Wait for the challenge to be solved
97 token_input = page.ele('@name=cf-turnstile-response', timeout=20)
98
99 if not token_input:
100 return ""
101
102 token = ""
103 for _ in range(40):
104 token = token_input.attr('value')
105 if token:
106 break
107 time.sleep(0.5)
108
109 return token
110 finally:
111 try:
112 page.quit()
113 except:
114 pass
115
116 class DeepInfra(OpenaiTemplate):
117 url = "https://deepinfra.com"
118 login_url = "https://deepinfra.com/dash/api_keys"
119 base_url = "https://api.deepinfra.com/v1/openai"
120
121 working = True
122 active_by_default = True
123
124 default_model = "zai-org/GLM-5.2"
125
126 @classmethod
127 def get_models(cls, **kwargs):
128 if not cls.models:
129 url = 'https://api.deepinfra.com/models/featured'
130 response = requests.get(url)
131 models = response.json()
132
133 cls.models = {model["model_name"]: {"id": model["model_name"], **model} for model in models if model.get("type") == "text-generation" or model.get("reported_type") == "text-to-image"}
134 cls.image_models = [model["model_name"] for model in models if model.get("reported_type") == "text-to-image"]
135 if cls.live == 0 and cls.models:
136 cls.live += 1
137 return cls.models
138
139 @classmethod
140 async def create_async_generator(cls, model, messages, api_key=None, headers=None, **kwargs):
141 if not api_key:
142 # Generate a Turnstile token for each request (required without an API key)
143 token = await get_turnstile_token_async()
144 if token:
145 if headers is None:
146 headers = {}
147 headers["X-DeepInfra-Turnstile"] = token
148 async for chunk in super().create_async_generator(model, messages, api_key=api_key, headers=headers, **kwargs):
149 yield chunk
150
151 @classmethod
152 def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict:
153 headers = super().get_headers(stream, api_key, headers)
154 if not api_key:
155 headers["X-Deepinfra-Source"] = "model-embed"
156 headers["Origin"] = "https://deepinfra.com"
157 headers["Referer"] = "https://deepinfra.com/"
158 if not headers.get("X-DeepInfra-Turnstile"):
159 token = get_turnstile_token()
160 if token:
161 headers["X-DeepInfra-Turnstile"] = token
162 return headers
Modified g4f/Provider/__init__.py +1 -1
@@ -33,7 +33,7 @@ __map_paths__ = {
33 33 "CopilotSession": "g4f.Provider.CopilotSession",
34 34 "CreateImagesProvider": "g4f.providers.create_images",
35 35 "Custom": "g4f.Provider.needs_auth.Custom",
36 "DeepInfra": "g4f.Provider.DeepInfra",
36 "DeepInfra": "g4f.Provider.deepinfra",
37 37 "DeepSeek": "g4f.Provider.needs_auth.DeepSeek",
38 38 "DeepSeekAPI": "g4f.Provider.needs_auth.DeepSeekAPI",
39 39 "EasyChat": "g4f.Provider.EasyChat",
Added g4f/Provider/deepinfra/__init__.py +130 -0
@@ -0,0 +1,130 @@
1 from __future__ import annotations
2
3 import asyncio
4 import requests
5 import time
6
7 from ..template import OpenaiTemplate
8 from .turnstile import Turnstile
9
10 def find_free_port() -> int:
11 import socket
12 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
13 s.bind(('', 0))
14 return s.getsockname()[1]
15
16 def get_turnstile_token_sync(model: str) -> str:
17 """Get Turnstile token using Turnstile with a fallback to DrissionPage."""
18 # 1. Try our lightweight CDP client first
19 try:
20 import os
21 import tempfile
22 port = find_free_port()
23 # Use a persistent temp directory to preserve browser session cookies and Turnstile reputation
24 user_data_dir = os.path.join(tempfile.gettempdir(), "g4f_chrome_profile_light")
25 client = Turnstile(port=port, user_data_dir=user_data_dir)
26 try:
27 token = client.get_token(model)
28 if token:
29 return token
30 print("[DeepInfra] Turnstile failed to obtain token. Trying fallback option (DrissionPage)...")
31 finally:
32 client.close()
33 except Exception as e:
34 print(f"[DeepInfra] Turnstile error: {e}. Trying fallback option (DrissionPage)...")
35
36 # 2. Fallback option: try original DrissionPage if installed
37 try:
38 from DrissionPage import ChromiumPage, ChromiumOptions
39 co = ChromiumOptions()
40 co.set_argument('--window-position=-2000,-2000')
41 co.set_argument('--window-size=1024,768')
42 co.set_argument('--log-level=3')
43 page = ChromiumPage(co)
44 try:
45 page.get(f'https://deepinfra.com/{model}')
46
47 # Block completions requests
48 js_block_fetch = """
49 const origFetch = window.fetch;
50 window.fetch = async function(...args) {
51 let url = args[0];
52 if (typeof url === 'string' && url.includes('/chat/completions')) {
53 return new Response('{}', {status: 200});
54 }
55 return origFetch.apply(this, args);
56 };
57 """
58 page.run_js(js_block_fetch)
59
60 textarea = page.ele('tag:textarea', timeout=15)
61 if textarea:
62 textarea.input('Test Prompt')
63 textarea.input('\n')
64
65 token_input = page.ele('@name=cf-turnstile-response', timeout=20)
66 if token_input:
67 for _ in range(40):
68 token = token_input.attr('value')
69 if token:
70 return token
71 time.sleep(0.5)
72 finally:
73 try:
74 page.quit()
75 except:
76 pass
77 except Exception as e:
78 print(f"[DeepInfra] Fallback DrissionPage method error: {e}")
79
80 return ""
81
82 async def get_turnstile_token_async() -> str:
83 loop = asyncio.get_running_loop()
84 return await loop.run_in_executor(None, get_turnstile_token_sync, DeepInfra.default_model)
85
86 class DeepInfra(OpenaiTemplate):
87 url = "https://deepinfra.com"
88 login_url = "https://deepinfra.com/dash/api_keys"
89 base_url = "https://api.deepinfra.com/v1/openai"
90
91 working = True
92 active_by_default = True
93
94 default_model = "zai-org/GLM-5.2"
95
96 @classmethod
97 def get_models(cls, **kwargs):
98 if not cls.models:
99 url = 'https://api.deepinfra.com/models/featured'
100 response = requests.get(url)
101 models = response.json()
102
103 cls.models = {model["model_name"]: {"id": model["model_name"], **model} for model in models if model.get("type") == "text-generation" or model.get("reported_type") == "text-to-image"}
104 cls.image_models = [model["model_name"] for model in models if model.get("reported_type") == "text-to-image"]
105 if cls.live == 0 and cls.models:
106 cls.live += 1
107
108 return cls.models
109
110 @classmethod
111 async def create_async_generator(cls, model, messages, api_key=None, headers=None, **kwargs):
112 if not api_key:
113 # Generate a Turnstile token for each request (required without an API key)
114 token = await get_turnstile_token_async()
115 if token:
116 if headers is None:
117 headers = {}
118 headers["X-DeepInfra-Turnstile"] = token
119
120 async for chunk in super().create_async_generator(model, messages, api_key=api_key, headers=headers, **kwargs):
121 yield chunk
122
123 @classmethod
124 def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict:
125 headers = super().get_headers(stream, api_key, headers)
126 if not api_key:
127 headers["X-Deepinfra-Source"] = "model-embed"
128 headers["Origin"] = "https://deepinfra.com"
129 headers["Referer"] = "https://deepinfra.com/"
130 return headers
Added g4f/Provider/deepinfra/turnstile.py +284 -0
@@ -0,0 +1,284 @@
1 import os
2 import shutil
3 import platform
4 import subprocess
5 import tempfile
6 import time
7 import json
8 import urllib.request
9 import urllib.error
10
11 def find_chrome_path():
12 """Search for Google Chrome or Chromium binary depending on OS."""
13 # Respect g4f's custom BrowserConfig.executable_path if configured
14 try:
15 from g4f.cookies import BrowserConfig
16 if BrowserConfig.executable_path and os.path.exists(BrowserConfig.executable_path):
17 return BrowserConfig.executable_path
18 except ImportError:
19 pass
20
21 # First, search using shutil.which
22 for name in ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']:
23 path = shutil.which(name)
24 if path:
25 return path
26
27 # System default paths
28 sys_name = platform.system().lower()
29 if sys_name == 'linux':
30 for path in ['/usr/bin/google-chrome', '/opt/google/chrome/google-chrome', '/usr/bin/chromium-browser']:
31 if os.path.exists(path):
32 return path
33 elif sys_name in ('macos', 'darwin'):
34 path = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
35 if os.path.exists(path):
36 return path
37 elif sys_name == 'windows':
38 paths = [
39 r"C:\Program Files\Google\Chrome\Application\chrome.exe",
40 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
41 ]
42 for path in paths:
43 if os.path.exists(path):
44 return path
45 return None
46
47 class Turnstile:
48 def __init__(self, port=9222, user_data_dir=None):
49 self.port = port
50
51 # Respect g4f's central cookies/cache directory if available
52 if user_data_dir is None:
53 try:
54 from g4f.cookies import get_cookies_dir
55 cookies_dir = get_cookies_dir()
56 if cookies_dir:
57 user_data_dir = os.path.join(cookies_dir, "chrome_profile_light")
58 except ImportError:
59 pass
60
61 # Default fallback to a persistent directory in temp folder
62 if user_data_dir is None:
63 user_data_dir = os.path.join(tempfile.gettempdir(), "g4f_chrome_profile_light")
64
65 self.user_data_dir = user_data_dir
66 self.process = None
67 self.ws = None
68 self.id_counter = 0
69
70 def start_chrome(self):
71 """Launch Chrome with CDP remote debugging port."""
72 chrome_path = find_chrome_path()
73 if not chrome_path:
74 raise RuntimeError("Google Chrome / Chromium executable not found.")
75
76 print(f"[Turnstile] Launching Chrome: {chrome_path} on port {self.port}")
77
78 # Create an isolated profile directory
79 os.makedirs(self.user_data_dir, exist_ok=True)
80
81 # Launch arguments matching DrissionPage to minimize detection
82 cmd = [
83 chrome_path,
84 f"--remote-debugging-port={self.port}",
85 f"--user-data-dir={self.user_data_dir}",
86 "--window-position=-2000,-2000",
87 "--window-size=1024,768",
88 "--no-default-browser-check",
89 "--disable-suggestions-ui",
90 "--no-first-run",
91 "--disable-infobars",
92 "--disable-popup-blocking",
93 "--hide-crash-restore-bubble",
94 "--disable-features=PrivacySandboxSettings4",
95 "--remote-allow-origins=*"
96 ]
97
98 self.process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
99
100 # Wait for CDP port readiness and retrieve the WebSocket URL
101 ws_url = None
102 for i in range(40): # Up to 20 seconds
103 time.sleep(0.5)
104 try:
105 with urllib.request.urlopen(f"http://127.0.0.1:{self.port}/json", timeout=2) as req:
106 targets = json.loads(req.read().decode('utf-8'))
107 for target in targets:
108 if target.get('type') in ('page', 'webview'):
109 ws_url = target.get('webSocketDebuggerUrl')
110 break
111 if ws_url:
112 break
113 except (urllib.error.URLError, ConnectionResetError, ConnectionRefusedError):
114 pass
115
116 if not ws_url:
117 self.close()
118 raise RuntimeError(f"Failed to connect to Chrome debugging port 127.0.0.1:{self.port}")
119
120 print(f"[Turnstile] Connected to CDP WebSocket: {ws_url}")
121 try:
122 from websocket import create_connection
123 except ImportError:
124 from g4f.errors import MissingRequirementsError
125 raise MissingRequirementsError('Install "websocket-client" package | pip install websocket-client')
126
127 self.ws = create_connection(ws_url)
128
129 # Enable necessary CDP domains
130 self.call_cdp("Page.enable")
131 self.call_cdp("DOM.enable")
132 self.call_cdp("Runtime.enable")
133 self.call_cdp("Emulation.setFocusEmulationEnabled", enabled=True)
134
135 def call_cdp(self, method, **params):
136 """Call CDP method and wait for response."""
137 self.id_counter += 1
138 payload = {
139 "id": self.id_counter,
140 "method": method,
141 "params": params
142 }
143 self.ws.send(json.dumps(payload))
144
145 while True:
146 response = json.loads(self.ws.recv())
147 if response.get("id") == self.id_counter:
148 if "error" in response:
149 raise RuntimeError(f"CDP Error calling {method}: {response['error']}")
150 return response.get("result", {})
151
152 def evaluate_js(self, expression):
153 """Execute JS code on the page and return the result."""
154 res = self.call_cdp("Runtime.evaluate", expression=expression, returnByValue=True)
155 return res.get("result", {}).get("value")
156
157 def get_token(self, model: str) -> str:
158 """Retrieve Turnstile token for the model."""
159 if not self.ws:
160 self.start_chrome()
161
162 target_url = f"https://deepinfra.com/{model}"
163 print(f"[Turnstile] Navigating to {target_url}...")
164 self.call_cdp("Page.navigate", url=target_url)
165
166 # Give some time to load
167 time.sleep(2.0)
168
169 # Inject completions request blocker ONLY in main frame (to avoid tampering with fetch inside Cloudflare's iframe)
170 fetch_blocker_js = """
171 const origFetch = window.fetch;
172 window.fetch = async function(...args) {
173 let url = args[0];
174 if (typeof url === 'string' && url.includes('/chat/completions')) {
175 return new Response('{}', {status: 200});
176 }
177 return origFetch.apply(this, args);
178 };
179 """
180 self.evaluate_js(fetch_blocker_js)
181
182 # Try to click "Accept" on cookies consent popup if present
183 self.evaluate_js("""
184 (() => {
185 const btn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Accept');
186 if (btn) btn.click();
187 })()
188 """)
189
190 # Wait for textarea readiness, focus and input text
191 print("[Turnstile] Waiting for active textarea...")
192 text_entered = False
193 for _ in range(40): # Up to 20 seconds
194 try:
195 ready = self.evaluate_js("""
196 (() => {
197 const ta = document.querySelector('textarea');
198 if (!ta) return 'no_textarea';
199 if (ta.disabled) return 'disabled';
200 ta.click();
201 ta.focus();
202 ta.scrollIntoView({ block: 'center' });
203 return 'ready';
204 })()
205 """)
206
207 if ready == 'ready':
208 print("[Turnstile] Textarea found, focusing and entering text...")
209
210 # Retrieve textarea nodeId for native focusing
211 doc = self.call_cdp('DOM.getDocument')
212 root_id = doc['root']['nodeId']
213 textarea = self.call_cdp('DOM.querySelector', nodeId=root_id, selector='textarea')
214
215 # Native focus via CDP
216 self.call_cdp('DOM.focus', nodeId=textarea['nodeId'])
217
218 # Enter text via native CDP command
219 self.call_cdp("Input.insertText", text="Test Prompt")
220
221 # Give React some time to process input before pressing Enter
222 time.sleep(0.5)
223
224 # Simulate Enter keypress via native CDP events (matching DrissionPage implementation)
225 self.call_cdp("Input.dispatchKeyEvent",
226 type="keyDown",
227 windowsVirtualKeyCode=13,
228 key="Enter",
229 code="Enter",
230 text="\r",
231 unmodifiedText="\r")
232 self.call_cdp("Input.dispatchKeyEvent",
233 type="keyUp",
234 windowsVirtualKeyCode=13,
235 key="Enter",
236 code="Enter",
237 text="\r",
238 unmodifiedText="\r")
239
240 text_entered = True
241 break
242 except Exception:
243 pass
244 time.sleep(0.5)
245
246 if not text_entered:
247 print("[-] Turnstile initiation error: textarea not found or disabled.")
248 return ""
249
250 # Poll page for Turnstile token presence
251 print("[Turnstile] Waiting for Cloudflare Turnstile solve...")
252 token_js = "document.querySelector('[name=cf-turnstile-response]') ? document.querySelector('[name=cf-turnstile-response]').value : ''"
253 token = ""
254 for i in range(120): # Up to 60 seconds
255 try:
256 token = self.evaluate_js(token_js)
257 if token:
258 print(f"[Turnstile] Token generated on check {i+1}!")
259 break
260 except Exception:
261 pass
262 time.sleep(0.5)
263
264 return token
265
266 def close(self):
267 """Close connection and terminate Chrome process."""
268 if self.ws:
269 try:
270 self.ws.close()
271 except Exception:
272 pass
273 self.ws = None
274
275 if self.process:
276 try:
277 self.process.terminate()
278 self.process.wait(timeout=5)
279 except Exception:
280 try:
281 self.process.kill()
282 except Exception:
283 pass
284 self.process = None