返回提交历史
Modified
g4f/Provider/OperaAria.py
+26
-16
Modified
g4f/image/__init__.py
+30
-13
Modified
g4f/image/copy_images.py
+4
-2
Modified
g4f/tools/media.py
+1
-1
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
代码差异
4 个文件
+61
-32
@@ -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()
@@ -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)
@@ -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:
@@ -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: