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

XFEstudio/gpt4free

feat(OperaAria): Update auth to mini-client, fix GUI image alt text, and address SSRF/mutable defaults (#3458)

* fix(OperaAria): update anonymous auth flow to use mini-client * feat(OperaAria): add support for think_harder reasoning model * fix(gui): restore missing is_safe_url in image init for backend_api * feat(OperaAria): yield Reasoning objects for GUI thinking status * fix(OperaAria): fix image alt text to prevent HTML breakage * revert(OperaAria): remove Reasoning yield as it is not actual reasoning * fix: address copilot feedback regarding SSRF, mutable defaults and string split errors * fix(image): update is_safe_url to block SSRF via socket and pass CI tests --------- Co-authored-by: kqlio67 <kqlio67@users.noreply.github.com>

798d8586
Kqlio <166700875+kqlio67@users.noreply.github.com>
提交于

代码差异

4 个文件 +61 -32
Modified g4f/Provider/OperaAria.py +26 -16
@@ -91,12 +91,14 @@ class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
91 91 async with session.post(
92 92 cls.token_endpoint,
93 93 headers={
94 "User-Agent": cls._user_agent_v1,
94 "User-Agent": "okhttp/5.3.2",
95 95 "Content-Type": "application/x-www-form-urlencoded",
96 "x-requested-with": "XMLHttpRequest",
97 "x-opera-client-cache": "1"
96 98 },
97 99 data={
98 "client_id": "ofa-client",
99 "client_secret": "N9OscfA3KxlJASuIe29PGZ5RpWaMTBoy",
100 "client_id": "mini-client",
101 "client_secret": "Pcc5NvlCrxl02pMw32kO6WrnhpS0pUZ95YrDP8XNKJJQvFht4wQDkFJ7v9x5hn7C",
100 102 "grant_type": "client_credentials",
101 103 "scope": "anonymous_account"
102 104 }
@@ -108,12 +110,14 @@ class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
108 110 async with session.post(
109 111 cls.signup_endpoint,
110 112 headers={
111 "User-Agent": "Mozilla 5.0 (Linux; Android 14) com.opera.browser OPR/89.5.4705.84314",
113 "User-Agent": "okhttp/5.3.2",
112 114 "Authorization": f"Bearer {anon_token}",
113 115 "Accept": "application/json",
114 116 "Content-Type": "application/json; charset=utf-8",
117 "x-requested-with": "XMLHttpRequest",
118 "x-opera-client-cache": "1"
115 119 },
116 json={"client_id": "ofa", "service": "aria"}
120 json={"client_id": "mini"}
117 121 ) as response:
118 122 response.raise_for_status()
119 123 auth_token = (await response.json())["token"]
@@ -122,15 +126,16 @@ class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
122 126 async with session.post(
123 127 cls.token_endpoint,
124 128 headers={
125 "User-Agent": cls._user_agent_v1,
129 "User-Agent": "okhttp/5.3.2",
126 130 "Content-Type": "application/x-www-form-urlencoded",
131 "x-requested-with": "XMLHttpRequest",
132 "x-opera-client-cache": "1"
127 133 },
128 134 data={
129 135 "auth_token": auth_token,
130 "client_id": "ofa",
131 "device_name": "GPT4FREE",
136 "client_id": "mini",
132 137 "grant_type": "auth_token",
133 "scope": "ALL"
138 "scope": "shodan:aria"
134 139 }
135 140 ) as response:
136 141 response.raise_for_status()
@@ -146,15 +151,20 @@ class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
146 151 return conversation.access_token
147 152
148 153 data = {
149 "client_id": "ofa",
154 "client_id": "mini",
150 155 "grant_type": "refresh_token",
151 156 "refresh_token": conversation.refresh_token,
152 "scope": "shodan:aria user:read"
157 "scope": "shodan:aria"
153 158 }
154 159
155 160 async with session.post(
156 161 cls.token_endpoint,
157 headers={"User-Agent": cls._user_agent_v1, "Content-Type": "application/x-www-form-urlencoded"},
162 headers={
163 "User-Agent": "okhttp/5.3.2",
164 "Content-Type": "application/x-www-form-urlencoded",
165 "x-requested-with": "XMLHttpRequest",
166 "x-opera-client-cache": "1"
167 },
158 168 data=data
159 169 ) as response:
160 170 response.raise_for_status()
@@ -290,7 +300,7 @@ class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
290 300 }
291 301
292 302 @classmethod
293 def _build_payload(cls, messages: Messages, conversation: Conversation, attachments: list, version: str) -> dict:
303 def _build_payload(cls, messages: Messages, conversation: Conversation, attachments: list, version: str, **kwargs) -> dict:
294 304 """Build request payload."""
295 305 query = format_prompt(messages)
296 306
@@ -308,7 +318,7 @@ class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
308 318 data = {
309 319 "query": query,
310 320 "sia": True,
311 "think_harder": False,
321 "think_harder": kwargs.get("think_harder", False),
312 322 "supported_features": [],
313 323 "file_attachments": attachments,
314 324 "encryption": {"key": conversation.encryption_key}
@@ -407,10 +417,10 @@ class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
407 417 attachments = await cls._process_media(session, access_token, media, messages)
408 418
409 419 headers = cls._build_headers(access_token, version)
410 payload = cls._build_payload(messages, conversation, attachments, version)
420 payload = cls._build_payload(messages, conversation, attachments, version, **kwargs)
411 421
412 422 # Save original prompt for ImageResponse alt text
413 original_prompt = prompt if prompt else format_prompt(messages)
423 original_prompt = prompt if prompt else (messages[-1]["content"] if messages else "")
414 424
415 425 async with session.post(api_endpoint, headers=headers, json=payload, proxy=proxy) as response:
416 426 response.raise_for_status()
Modified g4f/image/__init__.py +30 -13
@@ -10,6 +10,8 @@ from io import BytesIO
10 10 from pathlib import Path
11 11 from typing import Optional
12 12 from urllib.parse import urlparse
13 import socket
14 import ipaddress
13 15
14 16 import requests
15 17
@@ -118,6 +120,8 @@ def is_allowed_extension(filename: str) -> Optional[str]:
118 120
119 121 def is_safe_url(url: str) -> bool:
120 122 """Return True only for http/https URLs that do not point to private/loopback/reserved addresses."""
123 if not isinstance(url, str):
124 return False
121 125 try:
122 126 parsed = urlparse(url)
123 127
@@ -152,8 +156,6 @@ def is_safe_url(url: str) -> bool:
152 156 except Exception:
153 157 return False
154 158 return True
155
156
157 159 def is_data_an_media(data, filename: str = None) -> str:
158 160 content_type = is_data_an_audio(data, filename)
159 161 if content_type is not None:
@@ -387,6 +389,7 @@ def extract_data_uri(data_uri: str) -> bytes:
387 389 def process_image(image: Image.Image, new_width: int = 400, new_height: int = 400, save: str = None) -> Image.Image:
388 390 """
389 391 Processes the given image by adjusting its orientation and resizing it.
392 Preserves transparency for PNG output.
390 393
391 394 Args:
392 395 image (Image): The image to process.
@@ -440,6 +443,8 @@ def to_bytes(image: ImageType) -> bytes:
440 443 else:
441 444 raise FileNotFoundError(f"File not found: {path}")
442 445 else:
446 if not is_safe_url(image):
447 raise ValueError("Invalid or unsafe image url")
443 448 resp = requests.get(image, headers={
444 449 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
445 450 })
@@ -510,20 +515,32 @@ def get_width_height(
510 515 width: Optional[int] = None,
511 516 height: Optional[int] = None
512 517 ) -> tuple[int, int]:
513 if aspect_ratio == "1:1":
514 return width or 1024, height or 1024
515 elif aspect_ratio == "16:9":
516 return width or 832, height or 480
517 elif aspect_ratio == "9:16":
518 return width or 480, height or 832,
518 """
519 Returns (width, height) for common aspect ratios.
520 """
521 ratio_map = {
522 "1:1": (1024, 1024),
523 "16:9": (1024, 576),
524 "9:16": (576, 1024),
525 "4:3": (1024, 768),
526 "3:4": (768, 1024),
527 "3:2": (1024, 682),
528 "2:3": (682, 1024),
529 "21:9": (1024, 440),
530 "9:21": (440, 1024),
531 "4:5": (832, 1040),
532 "5:4": (1040, 832),
533 "2:1": (1024, 512),
534 "1:2": (512, 1024),
535 }
536 if aspect_ratio in ratio_map:
537 default_w, default_h = ratio_map[aspect_ratio]
538 return width or default_w, height or default_h
519 539 return width, height
520 540
521 541 class ImageRequest:
522 def __init__(
523 self,
524 options: dict = {}
525 ):
526 self.options = options
542 def __init__(self, options: dict = None):
543 self.options = options or {}
527 544
528 545 def get(self, key: str):
529 546 return self.options.get(key)
Modified g4f/image/copy_images.py +4 -2
@@ -13,7 +13,7 @@ from urllib.parse import urlparse
13 13
14 14 from ..typing import Optional, Cookies, Union
15 15 from ..requests.aiohttp import get_connector
16 from ..image import MEDIA_TYPE_MAP, EXTENSIONS_MAP
16 from ..image import MEDIA_TYPE_MAP, EXTENSIONS_MAP, is_safe_url
17 17 from ..tools.files import secure_filename
18 18 from ..providers.response import ImageResponse, AudioResponse, VideoResponse, quote_url
19 19 from . import is_accepted_format, extract_data_uri
@@ -68,7 +68,7 @@ def update_filename(response, filename: str) -> str:
68 68 async def save_response_media(
69 69 response,
70 70 prompt: str,
71 tags: list[str] = [],
71 tags: list[str] = None,
72 72 transcript: str = None,
73 73 content_type: str = None
74 74 ) -> AsyncIterator:
@@ -189,6 +189,8 @@ async def copy_media(
189 189 f.write(extract_data_uri(image))
190 190
191 191 elif not os.path.exists(target_path) or os.lstat(target_path).st_size <= 0:
192 if not is_safe_url(image):
193 raise ValueError(f"Invalid or unsafe image url: {image}")
192 194 async with session.get(image, ssl=ssl) as response:
193 195 response.raise_for_status()
194 196 if target is None:
Modified g4f/tools/media.py +1 -1
@@ -71,7 +71,7 @@ def merge_media(media: list, messages: list) -> Iterator:
71 71 image_url = image_url.get("url")
72 72 path: str = urlparse(image_url).path
73 73 if path.startswith("/files/"):
74 path = get_bucket_dir(path.split(path, "/")[1:])
74 path = get_bucket_dir(*path.split("/")[2:])
75 75 if os.path.exists(path):
76 76 buffer.append((Path(path), os.path.basename(path)))
77 77 else: