diff --git a/.env.example b/.env.example index 110fb18..9075c3b 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,4 @@ API_PREFIX=your_prefix CHATGPT_BASE_URL=https://chatgpt.com -HISTORY_DISABLED=true PROXY_URL=your_first_proxy, your_second_proxy -EXPORT_PROXY_URL=your_export_proxy -ARK0SE_TOKEN_URL=https://ark0se.example.com/token -POW_DIFFICULTY=000032 -RETRY_TIMES=3 -ENABLE_GATEWAY=true -CONVERSATION_ONLY=false -ENABLE_LIMIT=true -UPLOAD_BY_URL=false -SCHEDULED_REFRESH=false -USER_AGENTS=["ua1", "ua2"] \ No newline at end of file +SCHEDULED_REFRESH=false \ No newline at end of file diff --git a/README.md b/README.md index 13b195c..68f5db1 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ curl --location 'http://127.0.0.1:5005/v1/chat/completions' \ | | SCHEDULED_REFRESH | `false` | `false` | 是否定时刷新 `AccessToken` ,开启后每次启动程序将会全部非强制刷新一次,每4天晚上3点全部强制刷新一次。 | | | RANDOM_TOKEN | `true` | `true` | 是否随机选取后台 `Token` ,开启后随机后台账号,关闭后为顺序轮询 | | 网关功能 | ENABLE_GATEWAY | `false` | `false` | 是否启用网关模式,开启后可以使用镜像站,但也将会不设防 | +| | AUTO_SEED | `false` | `true` | 是否启用随机账号模式,默认启用,输入`seed`后随机匹配后台`Token`。关闭之后需要手动对接接口,来进行`Token`管控。 | ## 部署 diff --git a/api/chat2api.py b/api/chat2api.py new file mode 100644 index 0000000..62c965a --- /dev/null +++ b/api/chat2api.py @@ -0,0 +1,122 @@ +import asyncio +import types + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from fastapi import Request, Depends, HTTPException, Form +from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse +from starlette.background import BackgroundTask + +import utils.globals as globals +from app import templates, oauth2_scheme, app +from chatgpt.ChatService import ChatService +from chatgpt.authorization import refresh_all_tokens +from utils.Logger import logger +from utils.configs import api_prefix, scheduled_refresh +from utils.retry import async_retry + +scheduler = AsyncIOScheduler() + + +@app.on_event("startup") +async def app_start(): + if scheduled_refresh: + scheduler.add_job(id='refresh', func=refresh_all_tokens, trigger='cron', hour=3, minute=0, day='*/2', + kwargs={'force_refresh': True}) + scheduler.start() + asyncio.get_event_loop().call_later(0, lambda: asyncio.create_task(refresh_all_tokens(force_refresh=False))) + + +async def to_send_conversation(request_data, req_token): + chat_service = ChatService(req_token) + try: + await chat_service.set_dynamic_data(request_data) + await chat_service.get_chat_requirements() + return chat_service + except HTTPException as e: + await chat_service.close_client() + raise HTTPException(status_code=e.status_code, detail=e.detail) + except Exception as e: + await chat_service.close_client() + logger.error(f"Server error, {str(e)}") + raise HTTPException(status_code=500, detail="Server error") + + +async def process(request_data, req_token): + chat_service = await to_send_conversation(request_data, req_token) + await chat_service.prepare_send_conversation() + res = await chat_service.send_conversation() + return chat_service, res + + +@app.post(f"/{api_prefix}/v1/chat/completions" if api_prefix else "/v1/chat/completions") +async def send_conversation(request: Request, req_token: str = Depends(oauth2_scheme)): + try: + request_data = await request.json() + except Exception: + raise HTTPException(status_code=400, detail={"error": "Invalid JSON body"}) + chat_service, res = await async_retry(process, request_data, req_token) + try: + if isinstance(res, types.AsyncGeneratorType): + background = BackgroundTask(chat_service.close_client) + return StreamingResponse(res, media_type="text/event-stream", background=background) + else: + background = BackgroundTask(chat_service.close_client) + return JSONResponse(res, media_type="application/json", background=background) + except HTTPException as e: + await chat_service.close_client() + if e.status_code == 500: + logger.error(f"Server error, {str(e)}") + raise HTTPException(status_code=500, detail="Server error") + raise HTTPException(status_code=e.status_code, detail=e.detail) + except Exception as e: + await chat_service.close_client() + logger.error(f"Server error, {str(e)}") + raise HTTPException(status_code=500, detail="Server error") + + +@app.get(f"/{api_prefix}/tokens" if api_prefix else "/tokens", response_class=HTMLResponse) +async def upload_html(request: Request): + tokens_count = len(set(globals.token_list) - set(globals.error_token_list)) + return templates.TemplateResponse("tokens.html", + {"request": request, "api_prefix": api_prefix, "tokens_count": tokens_count}) + + +@app.post(f"/{api_prefix}/tokens/upload" if api_prefix else "/tokens/upload") +async def upload_post(text: str = Form(...)): + lines = text.split("\n") + for line in lines: + if line.strip() and not line.startswith("#"): + globals.token_list.append(line.strip()) + with open("../data/token.txt", "a", encoding="utf-8") as f: + f.write(line.strip() + "\n") + logger.info(f"Token count: {len(globals.token_list)}, Error token count: {len(globals.error_token_list)}") + tokens_count = len(set(globals.token_list) - set(globals.error_token_list)) + return {"status": "success", "tokens_count": tokens_count} + + +@app.post(f"/{api_prefix}/tokens/clear" if api_prefix else "/tokens/clear") +async def upload_post(): + globals.token_list.clear() + globals.error_token_list.clear() + with open("../data/token.txt", "w", encoding="utf-8") as f: + pass + logger.info(f"Token count: {len(globals.token_list)}, Error token count: {len(globals.error_token_list)}") + tokens_count = len(set(globals.token_list) - set(globals.error_token_list)) + return {"status": "success", "tokens_count": tokens_count} + + +@app.post(f"/{api_prefix}/tokens/error" if api_prefix else "/tokens/error") +async def error_tokens(): + error_tokens_list = list(set(globals.error_token_list)) + return {"status": "success", "error_tokens": error_tokens_list} + + +@app.get(f"/{api_prefix}/tokens/add/{{token}}" if api_prefix else "/tokens/add/{token}") +async def add_token(token: str): + if token.strip() and not token.startswith("#"): + globals.token_list.append(token.strip()) + with open("../data/token.txt", "a", encoding="utf-8") as f: + f.write(token.strip() + "\n") + logger.info(f"Token count: {len(globals.token_list)}, Error token count: {len(globals.error_token_list)}") + tokens_count = len(set(globals.token_list) - set(globals.error_token_list)) + return {"status": "success", "tokens_count": tokens_count} diff --git a/api/files.py b/api/files.py index c34de3f..c6f8fab 100644 --- a/api/files.py +++ b/api/files.py @@ -4,7 +4,7 @@ import pybase64 from PIL import Image from utils.Client import Client -from utils.config import export_proxy_url, cf_file_url +from utils.configs import export_proxy_url, cf_file_url async def get_file_content(url): diff --git a/app.py b/app.py index 118d8b3..e7d66af 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,13 @@ +import warnings + import uvicorn +from fastapi import FastAPI +from fastapi.security import OAuth2PasswordBearer +from fastapi.middleware.cors import CORSMiddleware +from fastapi.templating import Jinja2Templates + +warnings.filterwarnings("ignore") + log_config = uvicorn.config.LOGGING_CONFIG default_format = "%(asctime)s | %(levelname)s | %(message)s" @@ -6,5 +15,23 @@ access_format = r'%(asctime)s | %(levelname)s | %(client_addr)s: %(request_line) log_config["formatters"]["default"]["fmt"] = default_format log_config["formatters"]["access"]["fmt"] = access_format -uvicorn.run("chat2api:app", host="0.0.0.0", port=5005) -# uvicorn.run("chat2api:app", host="0.0.0.0", port=5005, ssl_keyfile="key.pem", ssl_certfile="cert.pem") +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +templates = Jinja2Templates(directory="templates") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) + +import gateway.backend +import gateway.share +import api.chat2api + +if __name__ == "__main__": + uvicorn.run("app:app", host="0.0.0.0", port=5005) + # uvicorn.run("app:app", host="0.0.0.0", port=5005, ssl_keyfile="key.pem", ssl_certfile="cert.pem") diff --git a/chat2api.py b/chat2api.py deleted file mode 100644 index 453896c..0000000 --- a/chat2api.py +++ /dev/null @@ -1,254 +0,0 @@ -import asyncio -import time -import types -import warnings - -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from fastapi import FastAPI, Request, Depends, HTTPException, Form -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse -from fastapi.responses import StreamingResponse, JSONResponse -from fastapi.security import OAuth2PasswordBearer -from fastapi.templating import Jinja2Templates -from starlette.background import BackgroundTask -from starlette.responses import RedirectResponse, Response - -from chatgpt.ChatService import ChatService -from chatgpt.authorization import refresh_all_tokens, verify_token, get_req_token -import chatgpt.globals as globals -from chatgpt.reverseProxy import chatgpt_reverse_proxy -from utils.Logger import logger -from utils.config import api_prefix, scheduled_refresh, enable_gateway -from utils.retry import async_retry - -warnings.filterwarnings("ignore") - -app = FastAPI() -scheduler = AsyncIOScheduler() -templates = Jinja2Templates(directory="templates") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.on_event("startup") -async def app_start(): - if scheduled_refresh: - scheduler.add_job(id='refresh', func=refresh_all_tokens, trigger='cron', hour=3, minute=0, day='*/4', - kwargs={'force_refresh': True}) - scheduler.start() - asyncio.get_event_loop().call_later(0, lambda: asyncio.create_task(refresh_all_tokens(force_refresh=False))) - - -async def to_send_conversation(request_data, req_token): - chat_service = ChatService(req_token) - try: - await chat_service.set_dynamic_data(request_data) - await chat_service.get_chat_requirements() - return chat_service - except HTTPException as e: - await chat_service.close_client() - raise HTTPException(status_code=e.status_code, detail=e.detail) - except Exception as e: - await chat_service.close_client() - logger.error(f"Server error, {str(e)}") - raise HTTPException(status_code=500, detail="Server error") - - -async def process(request_data, req_token): - chat_service = await to_send_conversation(request_data, req_token) - await chat_service.prepare_send_conversation() - res = await chat_service.send_conversation() - return chat_service, res - - -@app.post(f"/{api_prefix}/v1/chat/completions" if api_prefix else "/v1/chat/completions") -async def send_conversation(request: Request, req_token: str = Depends(oauth2_scheme)): - try: - request_data = await request.json() - except Exception: - raise HTTPException(status_code=400, detail={"error": "Invalid JSON body"}) - chat_service, res = await async_retry(process, request_data, req_token) - try: - if isinstance(res, types.AsyncGeneratorType): - background = BackgroundTask(chat_service.close_client) - return StreamingResponse(res, media_type="text/event-stream", background=background) - else: - background = BackgroundTask(chat_service.close_client) - return JSONResponse(res, media_type="application/json", background=background) - except HTTPException as e: - await chat_service.close_client() - if e.status_code == 500: - logger.error(f"Server error, {str(e)}") - raise HTTPException(status_code=500, detail="Server error") - raise HTTPException(status_code=e.status_code, detail=e.detail) - except Exception as e: - await chat_service.close_client() - logger.error(f"Server error, {str(e)}") - raise HTTPException(status_code=500, detail="Server error") - - -@app.get(f"/{api_prefix}/tokens" if api_prefix else "/tokens", response_class=HTMLResponse) -async def upload_html(request: Request): - tokens_count = len(set(globals.token_list) - set(globals.error_token_list)) - return templates.TemplateResponse("tokens.html", - {"request": request, "api_prefix": api_prefix, "tokens_count": tokens_count}) - - -@app.post(f"/{api_prefix}/tokens/upload" if api_prefix else "/tokens/upload") -async def upload_post(text: str = Form(...)): - lines = text.split("\n") - for line in lines: - if line.strip() and not line.startswith("#"): - globals.token_list.append(line.strip()) - with open("data/token.txt", "a", encoding="utf-8") as f: - f.write(line.strip() + "\n") - logger.info(f"Token count: {len(globals.token_list)}, Error token count: {len(globals.error_token_list)}") - tokens_count = len(set(globals.token_list) - set(globals.error_token_list)) - return {"status": "success", "tokens_count": tokens_count} - - -@app.post(f"/{api_prefix}/tokens/clear" if api_prefix else "/tokens/clear") -async def upload_post(): - globals.token_list.clear() - globals.error_token_list.clear() - with open("data/token.txt", "w", encoding="utf-8") as f: - pass - logger.info(f"Token count: {len(globals.token_list)}, Error token count: {len(globals.error_token_list)}") - tokens_count = len(set(globals.token_list) - set(globals.error_token_list)) - return {"status": "success", "tokens_count": tokens_count} - - -@app.post(f"/{api_prefix}/tokens/error" if api_prefix else "/tokens/error") -async def error_tokens(): - error_tokens_list = list(set(globals.error_token_list)) - return {"status": "success", "error_tokens": error_tokens_list} - - -@app.get(f"/{api_prefix}/tokens/add/{{token}}" if api_prefix else "/tokens/add/{token}") -async def add_token(token: str): - if token.strip() and not token.startswith("#"): - globals.token_list.append(token.strip()) - with open("data/token.txt", "a", encoding="utf-8") as f: - f.write(token.strip() + "\n") - logger.info(f"Token count: {len(globals.token_list)}, Error token count: {len(globals.error_token_list)}") - tokens_count = len(set(globals.token_list) - set(globals.error_token_list)) - return {"status": "success", "tokens_count": tokens_count} - - -if enable_gateway: - @app.get("/", response_class=HTMLResponse) - async def chatgpt_html(request: Request): - token = request.query_params.get("token") - if not token: - token = request.cookies.get("token") - if not token: - return await login_html(request) - - response = templates.TemplateResponse("chatgpt.html", {"request": request, "token": token}) - response.set_cookie("token", value=token) - return response - - @app.get("/login", response_class=HTMLResponse) - async def login_html(request: Request): - response = templates.TemplateResponse("login.html", {"request": request}) - return response - - - @app.get("/backend-api/gizmos/bootstrap") - async def get_gizmos_bootstrap(): - return {"gizmos": []} - - - # @app.get("/backend-api/conversations") - # async def get_conversations(): - # return {"items": [], "total": 0, "limit": 28, "offset": 0, "has_missing_conversations": False} - - # @app.patch("/backend-api/conversations") - # async def get_conversations(): - # return {"success": True, "message": None} - - - @app.get("/backend-api/me") - async def get_me(request: Request): - me = { - "object": "user", - "id": "org-chatgpt", - "email": "chatgpt@openai.com", - "name": "ChatGPT", - "picture": "https://cdn.auth0.com/avatars/ai.png", - "created": int(time.time()), - "phone_number": None, - "mfa_flag_enabled": False, - "amr": [], - "groups": [], - "orgs": { - "object": "list", - "data": [ - { - "object": "organization", - "id": "org-chatgpt", - "created": 1715641300, - "title": "Personal", - "name": "user-chatgpt", - "description": "Personal org for chatgpt@openai.com", - "personal": True, - "settings": {}, - "parent_org_id": None, - "is_default": False, - "role": "owner", - "is_scale_tier_authorized_purchaser": None, - "is_scim_managed": False, - "projects": { - "object": "list", - "data": [] - }, - "groups": [], - "geography": None - } - ] - }, - "has_payg_project_spend_limit": None - } - return me - - - banned_paths = [ - "backend-api/accounts/logout_all", - "backend-api/accounts/deactivate", - "backend-api/user_system_messages", - "backend-api/memories", - "backend-api/settings/clear_account_user_memory" - ] - redirect_paths = ["auth/logout"] - chatgpt_paths = ["c/"] - - - @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]) - async def reverse_proxy(request: Request, path: str): - for chatgpt_path in chatgpt_paths: - if chatgpt_path in path: - return await chatgpt_html(request) - - for banned_path in banned_paths: - if banned_path in path: - return Response(status_code=404) - - for redirect_path in redirect_paths: - if redirect_path in path: - redirect_url = str(request.base_url) - response = RedirectResponse(url=f"{redirect_url}", status_code=302) - response.delete_cookie("token") - return response - - return await chatgpt_reverse_proxy(request, path) -else: - @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]) - async def reverse_proxy(): - raise HTTPException(status_code=404, detail="Gateway is disabled") diff --git a/chatgpt/ChatService.py b/chatgpt/ChatService.py index d60b624..944c1f3 100644 --- a/chatgpt/ChatService.py +++ b/chatgpt/ChatService.py @@ -8,14 +8,14 @@ from starlette.concurrency import run_in_threadpool from api.files import get_image_size, get_file_extension, determine_file_use_case from api.models import model_proxy -from chatgpt.authorization import get_req_token, verify_token, get_ua +from chatgpt.authorization import get_req_token, verify_token, get_fp from chatgpt.chatFormat import api_messages_to_chat, stream_response, format_not_stream_response, head_process_response from chatgpt.chatLimit import check_is_limit, handle_request_limit from chatgpt.proofofWork import get_config, get_dpl, get_answer_token, get_requirements_token from utils.Client import Client from utils.Logger import logger -from utils.config import ( +from utils.configs import ( proxy_url_list, chatgpt_base_url_list, ark0se_token_url_list, @@ -26,7 +26,6 @@ from utils.config import ( upload_by_url, check_model, auth_key, - user_agents_list, turnstile_solver_url, ) @@ -35,20 +34,12 @@ class ChatService: def __init__(self, origin_token=None): # self.user_agent = random.choice(user_agents_list) if user_agents_list else "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" self.req_token = get_req_token(origin_token) - self.ua = get_ua(self.req_token) - self.user_agent = self.ua.get( - "user-agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", - ) self.chat_token = "gAAAAAB" self.s = None self.ws = None async def set_dynamic_data(self, data): if self.req_token: - logger.info(f"Request impersonate: {self.ua.get('impersonate')}") - logger.info(f"Request ua:{self.user_agent}") - logger.info(f"Request token: {self.req_token}") req_len = len(self.req_token.split(",")) if req_len == 1: self.access_token = await verify_token(self.req_token) @@ -61,6 +52,15 @@ class ChatService: self.access_token = None self.account_id = None + self.fp = get_fp(self.req_token) + self.proxy_url = self.fp.get("proxy_url") + self.user_agent = self.fp.get("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0") + self.impersonate = self.fp.get("impersonate", "safari15_3") + logger.info(f"Request token: {self.req_token}") + logger.info(f"Request proxy: {self.proxy_url}") + logger.info(f"Request UA: {self.user_agent}") + logger.info(f"Request impersonate: {self.impersonate}") + self.data = data await self.set_model() if enable_limit and self.req_token: @@ -79,11 +79,12 @@ class ChatService: if not isinstance(self.max_tokens, int): self.max_tokens = 2147483647 - self.proxy_url = random.choice(proxy_url_list) if proxy_url_list else None + # self.proxy_url = random.choice(proxy_url_list) if proxy_url_list else None + self.host_url = random.choice(chatgpt_base_url_list) if chatgpt_base_url_list else "https://chatgpt.com" self.ark0se_token_url = random.choice(ark0se_token_url_list) if ark0se_token_url_list else None - self.s = Client(proxy=self.proxy_url, impersonate=self.ua.get("impersonate", "safari15_3")) + self.s = Client(proxy=self.proxy_url, impersonate=self.impersonate) self.oai_device_id = str(uuid.uuid4()) self.persona = None @@ -104,9 +105,9 @@ class ChatService: 'origin': self.host_url, 'priority': 'u=1, i', 'referer': f'{self.host_url}/', - 'sec-ch-ua': self.ua.get("sec-ch-ua", '"Chromium";v="124", "Microsoft Edge";v="124", "Not-A.Brand";v="99"'), - 'sec-ch-ua-mobile': self.ua.get("sec-ch-ua-mobile", "?0"), - 'sec-ch-ua-platform': self.ua.get("sec-ch-ua-platform", '"Windows"'), + 'sec-ch-ua': self.fp.get("sec-ch-ua", '"Chromium";v="124", "Microsoft Edge";v="124", "Not-A.Brand";v="99"'), + 'sec-ch-ua-mobile': self.fp.get("sec-ch-ua-mobile", "?0"), + 'sec-ch-ua-platform': self.fp.get("sec-ch-ua-platform", '"Windows"'), 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin', @@ -114,9 +115,9 @@ class ChatService: } if self.access_token: self.base_url = self.host_url + "/backend-api" - self.base_headers['Authorization'] = f'Bearer {self.access_token}' + self.base_headers['authorization'] = f'Bearer {self.access_token}' if self.account_id: - self.base_headers['Chatgpt-Account-Id'] = self.account_id + self.base_headers['chatgpt-account-id'] = self.account_id else: self.base_url = self.host_url + "/backend-anon" @@ -224,7 +225,7 @@ class ChatService: if not self.ark0se_token_url: raise HTTPException(status_code=403, detail="Ark0se service required") ark0se_dx = ark0se.get("dx") - ark0se_client = Client(impersonate=self.ua.get("impersonate", "safari15_3")) + ark0se_client = Client(impersonate=self.fp.get("impersonate", "safari15_3")) try: r2 = await ark0se_client.post( url=self.ark0se_token_url, json={"blob": ark0se_dx, "method": ark0se_method}, timeout=15 @@ -382,7 +383,7 @@ class ChatService: url = f"{self.base_url}/files/{file_id}/download" headers = self.base_headers.copy() try: - r = await self.s.get(url, headers=headers, timeout=5) + r = await self.s.get(url, headers=headers, timeout=10) if r.status_code == 200: download_url = r.json().get('download_url') return download_url @@ -396,7 +397,7 @@ class ChatService: url = f"{self.base_url}/files/{file_id}/uploaded" headers = self.base_headers.copy() try: - r = await self.s.post(url, headers=headers, json={}, timeout=30) + r = await self.s.post(url, headers=headers, json={}, timeout=10) if r.status_code == 200: download_url = r.json().get('download_url') return download_url @@ -413,7 +414,7 @@ class ChatService: r = await self.s.post( url, headers=headers, - json={"file_name": file_name, "file_size": file_size, "timezone_offset_min": -480, "use_case": use_case}, + json={"file_name": file_name, "file_size": file_size, "reset_rate_limits": False, "timezone_offset_min": -480, "use_case": use_case}, timeout=5, ) if r.status_code == 200: @@ -438,7 +439,9 @@ class ChatService: 'x-ms-version': '2020-04-08', } ) - headers.pop('Authorization', None) + headers.pop('authorization', None) + headers.pop('oai-device-id', None) + headers.pop('oai-language', None) try: r = await self.s.put(upload_url, headers=headers, data=file_content, timeout=60) if r.status_code == 201: diff --git a/chatgpt/authorization.py b/chatgpt/authorization.py index af6d616..8b7016e 100644 --- a/chatgpt/authorization.py +++ b/chatgpt/authorization.py @@ -1,77 +1,97 @@ import asyncio import json -import os import random import ua_generator +from ua_generator.options import Options +from ua_generator.data.version import VersionRange from fastapi import HTTPException -import chatgpt.globals as globals +import utils.configs as configs +import utils.globals as globals from chatgpt.refreshToken import rt2ac from utils.Logger import logger -from utils.config import authorization_list, random_token - -os.environ['PYTHONHASHSEED'] = '0' -random.seed(0) def get_req_token(req_token, seed=None): - available_token_list = list(set(globals.token_list) - set(globals.error_token_list)) - length = len(available_token_list) - if seed and length > 0: - req_token = globals.token_list[hash(seed) % length] - while req_token in globals.error_token_list: - req_token = random.choice(globals.token_list) - return req_token - - if req_token in authorization_list: - if len(available_token_list) > 0: - if random_token: - req_token = random.choice(available_token_list) - return req_token + if configs.auto_seed or not seed: + available_token_list = list(set(globals.token_list) - set(globals.error_token_list)) + length = len(available_token_list) + if seed and length > 0: + if seed not in globals.seed_map.keys(): + globals.seed_map[seed] = {"token": random.choice(available_token_list), "conversations": []} + with open(globals.SEED_MAP_FILE, "w") as f: + json.dump(globals.seed_map, f, indent=4) else: - globals.count += 1 - globals.count %= length - return available_token_list[globals.count] + req_token = globals.seed_map[seed]["token"] + return req_token + + if req_token in configs.authorization_list: + if len(available_token_list) > 0: + if configs.random_token: + req_token = random.choice(available_token_list) + return req_token + else: + globals.count += 1 + globals.count %= length + return available_token_list[globals.count] + else: + return None else: - return None + return req_token else: - return req_token + if seed not in globals.seed_map.keys(): + raise HTTPException(status_code=401, detail={"error": "Invalid Seed"}) + return globals.seed_map[seed]["token"] -def get_ua(req_token): - user_agent = globals.user_agent_map.get(req_token, {}) - user_agent = {k.lower(): v for k, v in user_agent.items()} - if not user_agent: - if not req_token: - ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge'), platform=('windows', 'macos')) - return { - "user-agent": ua.text, - "sec-ch-ua-platform": ua.platform, - "sec-ch-ua": ua.ch.brands, - "sec-ch-ua-mobile": ua.ch.mobile, - "impersonate": random.choice(globals.impersonate_list), - } - else: - ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge'), platform=('windows', 'macos')) - user_agent = { - "user-agent": ua.text, - "sec-ch-ua-platform": ua.platform, - "sec-ch-ua": ua.ch.brands, - "sec-ch-ua-mobile": ua.ch.mobile, - "impersonate": random.choice(globals.impersonate_list), - } - globals.user_agent_map[req_token] = user_agent - with open(globals.USER_AGENTS_FILE, "w", encoding="utf-8") as f: - f.write(json.dumps(globals.user_agent_map, indent=4)) - return user_agent - else: +def get_fp(req_token): + fp = globals.user_agent_map.get(req_token, {}) + if fp and fp.get("user-agent") and fp.get("impersonate"): + if "proxy_url" in fp.keys() and fp["proxy_url"] and fp["proxy_url"] not in configs.proxy_url_list: + fp["proxy_url"] = random.choice(configs.proxy_url_list) if configs.proxy_url_list else None + globals.user_agent_map[req_token] = fp + with open(globals.FP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.user_agent_map, f, indent=4) + if globals.impersonate_list and "impersonate" in fp.keys() and fp["impersonate"] not in globals.impersonate_list: + fp["impersonate"] = random.choice(globals.impersonate_list) + globals.user_agent_map[req_token] = fp + with open(globals.FP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.user_agent_map, f, indent=4) + if configs.user_agents_list and "user-agent" in fp.keys() and fp["user-agent"] not in configs.user_agents_list: + fp["user-agent"] = random.choice(configs.user_agents_list) + globals.user_agent_map[req_token] = fp + with open(globals.FP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.user_agent_map, f, indent=4) + user_agent = {k.lower(): v for k, v in fp.items()} return user_agent + else: + options = Options(version_ranges={ + 'chrome': VersionRange(min_version=124), + 'edge': VersionRange(min_version=124), + }) + ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge', 'firefox', 'safari'), + platform=('windows', 'macos'), options=options) + fp = { + "user-agent": ua.text if not configs.user_agents_list else random.choice(configs.user_agents_list), + "sec-ch-ua-platform": ua.ch.platform, + "sec-ch-ua": ua.ch.brands, + "sec-ch-ua-mobile": ua.ch.mobile, + "impersonate": random.choice(globals.impersonate_list), + "proxy_url": random.choice(configs.proxy_url_list) if configs.proxy_url_list else None, + } + if not req_token: + return fp + else: + globals.user_agent_map[req_token] = fp + with open(globals.FP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.user_agent_map, f, indent=4) + return fp async def verify_token(req_token): if not req_token: - if authorization_list: + if configs.authorization_list: logger.error("Unauthorized with empty token.") raise HTTPException(status_code=401) else: @@ -94,7 +114,7 @@ async def refresh_all_tokens(force_refresh=False): for token in list(set(globals.token_list) - set(globals.error_token_list)): if len(token) == 45: try: - await asyncio.sleep(2) + await asyncio.sleep(0.5) await rt2ac(token, force_refresh=force_refresh) except HTTPException: pass diff --git a/chatgpt/globals.py b/chatgpt/globals.py deleted file mode 100644 index 248600a..0000000 --- a/chatgpt/globals.py +++ /dev/null @@ -1,108 +0,0 @@ -import json -import os - -import ua_generator -import random - -from utils.Logger import logger - -DATA_FOLDER = "data" -TOKENS_FILE = os.path.join(DATA_FOLDER, "token.txt") -REFRESH_MAP_FILE = os.path.join(DATA_FOLDER, "refresh_map.json") -ERROR_TOKENS_FILE = os.path.join(DATA_FOLDER, "error_token.txt") -WSS_MAP_FILE = os.path.join(DATA_FOLDER, "wss_map.json") -USER_AGENTS_FILE = os.path.join(DATA_FOLDER, "user_agents.json") - -count = 0 -token_list = [] -error_token_list = [] -refresh_map = {} -wss_map = {} -user_agent_map = {} -impersonate_list = [ - "chrome99", - "chrome100", - "chrome101", - "chrome104", - "chrome107", - "chrome110", - "chrome116", - "chrome119", - "chrome120", - "chrome123", - "edge99", - "edge101", -] - -if not os.path.exists(DATA_FOLDER): - os.makedirs(DATA_FOLDER) - -if os.path.exists(REFRESH_MAP_FILE): - with open(REFRESH_MAP_FILE, "r") as file: - refresh_map = json.load(file) -else: - refresh_map = {} - -if os.path.exists(WSS_MAP_FILE): - with open(WSS_MAP_FILE, "r") as file: - wss_map = json.load(file) -else: - wss_map = {} - - -if os.path.exists(TOKENS_FILE): - with open(TOKENS_FILE, "r", encoding="utf-8") as f: - for line in f: - if line.strip() and not line.startswith("#"): - token_list.append(line.strip()) -else: - with open(TOKENS_FILE, "w", encoding="utf-8") as f: - pass - - -if os.path.exists(ERROR_TOKENS_FILE): - with open(ERROR_TOKENS_FILE, "r", encoding="utf-8") as f: - for line in f: - if line.strip() and not line.startswith("#"): - error_token_list.append(line.strip()) -else: - with open(ERROR_TOKENS_FILE, "w", encoding="utf-8") as f: - pass - -if os.path.exists(USER_AGENTS_FILE): - with open(USER_AGENTS_FILE, "r", encoding="utf-8") as f: - try: - user_agent_map = json.load(f) - except json.JSONDecodeError: - user_agent_map = {} - # token数量变化时,更新ua - if len(user_agent_map.keys()) != len(token_list): - new_tokens = list(set(token_list) - user_agent_map.keys()) - for token in new_tokens: - ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge'), platform=('windows', 'macos')) - ua_dict = { - "user-agent": ua.text, - "sec-ch-ua-platform": ua.platform, - "sec-ch-ua": ua.ch.brands, - "sec-ch-ua-mobile": ua.ch.mobile, - "impersonate": random.choice(impersonate_list), - } - user_agent_map[token] = ua_dict - with open(USER_AGENTS_FILE, "w", encoding="utf-8") as f: - f.write(json.dumps(user_agent_map, indent=4)) -else: - for token in token_list: - ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge'), platform=('windows', 'macos')) - ua_dict = { - "user-agent": ua.text, - "sec-ch-ua-platform": ua.platform, - "sec-ch-ua": ua.ch.brands, - "sec-ch-ua-mobile": ua.ch.mobile, - "impersonate": random.choice(impersonate_list), - } - user_agent_map[token] = ua_dict - with open(USER_AGENTS_FILE, "w", encoding="utf-8") as f: - f.write(json.dumps(user_agent_map, indent=4)) - -if token_list: - logger.info(f"Token list count: {len(token_list)}, Error token list count: {len(error_token_list)}") diff --git a/chatgpt/proofofWork.py b/chatgpt/proofofWork.py index 7ab56bc..ec184c2 100644 --- a/chatgpt/proofofWork.py +++ b/chatgpt/proofofWork.py @@ -10,7 +10,7 @@ from html.parser import HTMLParser import pybase64 from utils.Logger import logger -from utils.config import conversation_only +from utils.configs import conversation_only cores = [16, 24, 32] screens = [3000, 4000, 6000] diff --git a/chatgpt/refreshToken.py b/chatgpt/refreshToken.py index 86a7b14..8bc11ee 100644 --- a/chatgpt/refreshToken.py +++ b/chatgpt/refreshToken.py @@ -6,25 +6,21 @@ from fastapi import HTTPException from utils.Client import Client from utils.Logger import logger -from utils.config import proxy_url_list -import chatgpt.globals as globals - - -def save_refresh_map(refresh_map): - with open(globals.REFRESH_MAP_FILE, "w") as file: - json.dump(refresh_map, file) +from utils.configs import proxy_url_list +import utils.globals as globals async def rt2ac(refresh_token, force_refresh=False): if not force_refresh and (refresh_token in globals.refresh_map and int(time.time()) - globals.refresh_map.get(refresh_token, {}).get("timestamp", 0) < 5 * 24 * 60 * 60): access_token = globals.refresh_map[refresh_token]["token"] - logger.info(f"refresh_token -> access_token from cache") + # logger.info(f"refresh_token -> access_token from cache") return access_token else: try: access_token = await chat_refresh(refresh_token) globals.refresh_map[refresh_token] = {"token": access_token, "timestamp": int(time.time())} - save_refresh_map(globals.refresh_map) + with open(globals.REFRESH_MAP_FILE, "w") as f: + json.dump(globals.refresh_map, f, indent=4) logger.info(f"refresh_token -> access_token with openai: {access_token}") return access_token except HTTPException as e: diff --git a/chatgpt/wssClient.py b/chatgpt/wssClient.py index 0c60f82..3081a47 100644 --- a/chatgpt/wssClient.py +++ b/chatgpt/wssClient.py @@ -2,12 +2,12 @@ import json import time from utils.Logger import logger -import chatgpt.globals as globals +import utils.globals as globals def save_wss_map(wss_map): - with open(globals.WSS_MAP_FILE, "w") as file: - json.dump(wss_map, file) + with open(globals.WSS_MAP_FILE, "w") as f: + json.dump(wss_map, f, indent=4) async def token2wss(token): diff --git a/gateway/backend.py b/gateway/backend.py new file mode 100644 index 0000000..5820500 --- /dev/null +++ b/gateway/backend.py @@ -0,0 +1,332 @@ +import json +import re +import time +import uuid + +from fastapi import Request, HTTPException +from fastapi.responses import HTMLResponse, RedirectResponse, Response + +import utils.globals as globals +from app import app, templates +from gateway.reverseProxy import chatgpt_reverse_proxy +from utils.configs import authorization_list +from utils.configs import enable_gateway + +with open("templates/remix_context.json", "r", encoding="utf-8") as f: + remix_context = json.load(f) + + +def set_value_for_key(data, target_key, new_value): + if isinstance(data, dict): + for key, value in data.items(): + if key == target_key: + data[key] = new_value + else: + set_value_for_key(value, target_key, new_value) + elif isinstance(data, list): + for item in data: + set_value_for_key(item, target_key, new_value) + + +if enable_gateway: + @app.get("/", response_class=HTMLResponse) + async def chatgpt_html(request: Request): + token = request.query_params.get("token") + if not token: + token = request.cookies.get("token") + if not token: + return await login_html(request) + + user_remix_context = remix_context.copy() + set_value_for_key(user_remix_context, "user", {"id": "user-chatgpt"}) + set_value_for_key(user_remix_context, "accessToken", token) + + response = templates.TemplateResponse("chatgpt.html", {"request": request, "remix_context": user_remix_context}) + response.set_cookie("token", value=token, expires="Thu, 01 Jan 2099 00:00:00 GMT") + return response + + # @app.get("/backend-api/accounts/check/v4-2023-04-27") + # async def check_account(request: Request): + # token = request.headers.get("Authorization").replace("Bearer ", "") + # check_account_response = await chatgpt_reverse_proxy(request, "backend-api/accounts/check/v4-2023-04-27") + # if len(token) == 45 or token.startswith("eyJhbGciOi"): + # return check_account_response + # else: + # check_account_str = check_account_response.body.decode('utf-8') + # check_account_info = json.loads(check_account_str) + # for key in check_account_info.get("accounts", {}).keys(): + # account_id = check_account_info["accounts"][key]["account"]["account_id"] + # globals.seed_map[token]["user_id"] = check_account_info["accounts"][key]["account"]["account_user_id"].split("__")[0] + # check_account_info["accounts"][key]["account"]["account_user_id"] = f"user-chatgpt__{account_id}" + # with open(globals.SEED_MAP_FILE, "w", encoding="utf-8") as f: + # json.dump(globals.seed_map, f, indent=4) + # return check_account_info + + + def verify_authorization(request: Request): + auth_header = request.headers.get("Authorization").replace("Bearer ", "") + + if not auth_header: + raise HTTPException(status_code=401, detail="Authorization header is missing") + if auth_header not in authorization_list: + raise HTTPException(status_code=401, detail="Invalid authorization") + + + @app.get("/seedtoken") + async def get_seedtoken(request: Request): + verify_authorization(request) + try: + params = request.query_params + seed = params.get("seed") + + if seed: + if seed not in globals.seed_map: + raise HTTPException(status_code=404, detail=f"Seed '{seed}' not found") + return { + "status": "success", + "data": { + "seed": seed, + "token": globals.seed_map[seed]["token"] + } + } + + token_map = { + seed: data["token"] + for seed, data in globals.seed_map.items() + } + return {"status": "success", "data": token_map} + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + + + @app.post("/seedtoken") + async def set_seedtoken(request: Request): + verify_authorization(request) + data = await request.json() + + seed = data.get("seed") + token = data.get("token") + + if seed not in globals.seed_map: + globals.seed_map[seed] = { + "token": token, + "conversations": [] + } + else: + globals.seed_map[seed]["token"] = token + + with open(globals.SEED_MAP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.seed_map, f, indent=4) + + return {"status": "success", "message": "Token updated successfully"} + + + @app.delete("/seedtoken") + async def delete_seedtoken(request: Request): + verify_authorization(request) + + try: + data = await request.json() + seed = data.get("seed") + + if seed == "clear": + globals.seed_map.clear() + with open(globals.SEED_MAP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.seed_map, f, indent=4) + return {"status": "success", "message": "All seeds deleted successfully"} + + if not seed: + raise HTTPException(status_code=400, detail="Missing required field: seed") + + if seed not in globals.seed_map: + raise HTTPException(status_code=404, detail=f"Seed '{seed}' not found") + del globals.seed_map[seed] + + with open(globals.SEED_MAP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.seed_map, f, indent=4) + + return { + "status": "success", + "message": f"Seed '{seed}' deleted successfully" + } + + except json.JSONDecodeError: + raise HTTPException(status_code=400, detail="Invalid JSON data") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + + + @app.get("/login", response_class=HTMLResponse) + async def login_html(request: Request): + response = templates.TemplateResponse("login.html", {"request": request}) + return response + + + @app.get("/gpts") + async def get_gpts(): + return {"kind": "store"} + + + @app.get("/backend-api/gizmos/bootstrap") + async def get_gizmos_bootstrap(): + return {"gizmos": []} + + + @app.get("/backend-api/conversations") + async def get_conversations(request: Request): + limit = int(request.query_params.get("limit", 28)) + offset = int(request.query_params.get("offset", 0)) + token = request.headers.get("Authorization", "").replace("Bearer ", "") + if len(token) == 45 or token.startswith("eyJhbGciOi"): + return await chatgpt_reverse_proxy(request, "backend-api/conversations") + else: + items = [] + for conversation_id in globals.seed_map.get(token, {}).get("conversations", []): + conversation = globals.conversation_map.get(conversation_id, None) + if conversation: + items.append(conversation) + items = items[int(offset):int(offset) + int(limit)] + conversations = { + "items": items, + "total": len(items), + "limit": limit, + "offset": offset, + "has_missing_conversations": False + } + return conversations + + + @app.get("/backend-api/conversation/{conversation_id}") + async def update_conversation(request: Request, conversation_id: str): + token = request.headers.get("Authorization", "").replace("Bearer ", "") + conversation_details_response = await chatgpt_reverse_proxy(request, + f"backend-api/conversation/{conversation_id}") + if len(token) == 45 or token.startswith("eyJhbGciOi"): + return conversation_details_response + else: + conversation_details_str = conversation_details_response.body.decode('utf-8') + conversation_details = json.loads(conversation_details_str) + if conversation_id in globals.seed_map[token][ + "conversations"] and conversation_id in globals.conversation_map: + globals.conversation_map[conversation_id]["title"] = conversation_details.get("title", None) + globals.conversation_map[conversation_id]["is_archived"] = conversation_details.get("is_archived", + False) + with open(globals.CONVERSATION_MAP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.conversation_map, f, indent=4) + return conversation_details_response + + + @app.patch("/backend-api/conversation/{conversation_id}") + async def patch_conversation(request: Request, conversation_id: str): + token = request.headers.get("Authorization", "").replace("Bearer ", "") + patch_response = (await chatgpt_reverse_proxy(request, f"backend-api/conversation/{conversation_id}")) + if len(token) == 45 or token.startswith("eyJhbGciOi"): + return patch_response + else: + data = await request.json() + if conversation_id in globals.seed_map[token][ + "conversations"] and conversation_id in globals.conversation_map: + if not data.get("is_visible", True): + globals.conversation_map.pop(conversation_id) + globals.seed_map[token]["conversations"].remove(conversation_id) + with open(globals.SEED_MAP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.seed_map, f, indent=4) + else: + globals.conversation_map[conversation_id].update(data) + with open(globals.CONVERSATION_MAP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.conversation_map, f, indent=4) + return patch_response + + @app.get("/backend-api/me") + async def get_me(request: Request): + me = { + "object": "user", + "id": "org-chatgpt", + "email": "chatgpt@openai.com", + "name": "ChatGPT", + "picture": "https://cdn.auth0.com/avatars/ai.png", + "created": int(time.time()), + "phone_number": None, + "mfa_flag_enabled": False, + "amr": [], + "groups": [], + "orgs": { + "object": "list", + "data": [ + { + "object": "organization", + "id": "org-chatgpt", + "created": 1715641300, + "title": "Personal", + "name": "user-chatgpt", + "description": "Personal org for chatgpt@openai.com", + "personal": True, + "settings": { + "threads_ui_visibility": "NONE", + "usage_dashboard_visibility": "ANY_ROLE", + "disable_user_api_keys": False + }, + "parent_org_id": None, + "is_default": True, + "role": "owner", + "is_scale_tier_authorized_purchaser": None, + "is_scim_managed": False, + "projects": { + "object": "list", + "data": [] + }, + "groups": [], + "geography": None + } + ] + }, + "has_payg_project_spend_limit": True + } + return me + + + banned_paths = [ + "backend-api/accounts/logout_all", + "backend-api/accounts/deactivate", + "backend-api/user_system_messages", + "backend-api/memories", + "backend-api/settings/clear_account_user_memory", + "backend-api/conversations/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + "backend-api/accounts/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/invites", + "admin", + ] + redirect_paths = ["auth/logout"] + chatgpt_paths = ["c/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"] + + + @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]) + async def reverse_proxy(request: Request, path: str): + if re.match("/v1/rgstr", path): + return Response(status_code=202, content=b'{"success":true}') + + if re.match("ces/v1", path): + return {"success": True} + + if re.match("backend-api/edge", path): + return Response(status_code=204) + + for chatgpt_path in chatgpt_paths: + if re.match(chatgpt_path, path): + return await chatgpt_html(request) + + for banned_path in banned_paths: + if re.match(banned_path, path): + raise HTTPException(status_code=403, detail="Forbidden") + + for redirect_path in redirect_paths: + if re.match(redirect_path, path): + redirect_url = str(request.base_url) + response = RedirectResponse(url=f"{redirect_url}login", status_code=302) + return response + + return await chatgpt_reverse_proxy(request, path) +else: + @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]) + async def reverse_proxy(): + raise HTTPException(status_code=404, detail="Gateway is disabled") diff --git a/chatgpt/reverseProxy.py b/gateway/reverseProxy.py similarity index 52% rename from chatgpt/reverseProxy.py rename to gateway/reverseProxy.py index c6d7b0a..296d346 100644 --- a/chatgpt/reverseProxy.py +++ b/gateway/reverseProxy.py @@ -1,13 +1,24 @@ import json import random +import time from fastapi import Request, HTTPException from fastapi.responses import StreamingResponse, Response from starlette.background import BackgroundTask -from chatgpt.authorization import verify_token, get_req_token, get_ua +from chatgpt.authorization import verify_token, get_req_token, get_fp +import utils.globals as globals from utils.Client import Client -from utils.config import chatgpt_base_url_list, proxy_url_list, enable_gateway +from utils.Logger import logger +from utils.configs import chatgpt_base_url_list, proxy_url_list + + +from datetime import datetime, timezone + +def generate_current_time(): + current_time = datetime.now(timezone.utc) + formatted_time = current_time.isoformat(timespec='microseconds').replace('+00:00', 'Z') + return formatted_time headers_reject_list = [ "x-real-ip", @@ -70,6 +81,60 @@ async def get_real_req_token(token): return req_token +async def content_generator(r, token): + conversation_id = None + title = None + async for chunk in r.aiter_content(): + try: + if (len(token) != 45 and not token.startswith("eyJhbGciOi")) and (not conversation_id or not title): + chat_chunk = chunk.decode('utf-8') + if "title" in chat_chunk: + pass + if chat_chunk.startswith("data: {"): + if "\n\nevent: delta" in chat_chunk: + index = chat_chunk.find("\n\nevent: delta") + chunk_data = chat_chunk[6:index] + elif "\n\ndata: {" in chat_chunk: + index = chat_chunk.find("\n\ndata: {") + chunk_data = chat_chunk[6:index] + else: + chunk_data = chat_chunk[6:] + chunk_data = chunk_data.strip() + if conversation_id is None: + conversation_id = json.loads(chunk_data).get("conversation_id") + if conversation_id in globals.conversation_map: + title = globals.conversation_map[conversation_id].get("title") + if title is None: + title = json.loads(chunk_data).get("title") + + if conversation_id and title: + conversation_detail = { + "id": conversation_id, + "title": title, + "update_time": generate_current_time(), + "workspace_id": None, + } + if conversation_id not in globals.conversation_map: + globals.conversation_map[conversation_id] = conversation_detail + else: + globals.conversation_map[conversation_id]["update_time"] = generate_current_time() + if conversation_id not in globals.seed_map[token]["conversations"]: + globals.seed_map[token]["conversations"].insert(0, conversation_id) + else: + globals.seed_map[token]["conversations"].remove(conversation_id) + globals.seed_map[token]["conversations"].insert(0, conversation_id) + with open(globals.CONVERSATION_MAP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.conversation_map, f, indent=4) + with open(globals.SEED_MAP_FILE, "w", encoding="utf-8") as f: + json.dump(globals.seed_map, f, indent=4) + logger.info(f"Conversation ID: {conversation_id}, Title: {title}") + except Exception as e: + # logger.error(e) + # logger.error(chunk.decode('utf-8')) + pass + yield chunk + + async def chatgpt_reverse_proxy(request: Request, path: str): try: origin_host = request.url.netloc @@ -96,11 +161,16 @@ async def chatgpt_reverse_proxy(request: Request, path: str): base_url = "https://cdn.oaistatic.com" if "file-" in path and "backend-api" not in path: base_url = "https://files.oaiusercontent.com" + if "v1/" in path: + base_url = "https://ab.chatgpt.com" - token = request.cookies.get("token") + token = request.cookies.get("token", "") req_token = await get_real_req_token(token) - ua = get_ua(req_token) - headers.update(ua) + fp = get_fp(req_token) + proxy_url = fp.get("proxy_url") + user_agent = fp.get("user-agent") + impersonate = fp.get("impersonate", "safari15_3") + headers.update(fp) headers.update({ "accept-language": "en-US,en;q=0.9", @@ -108,6 +178,13 @@ async def chatgpt_reverse_proxy(request: Request, path: str): "origin": base_url, "referer": f"{base_url}/" }) + if "ab.chatgpt.com" in base_url: + headers.update({ + "statsig-sdk-type": "js-client", + "statsig-api-key": "client-tnE5GCU2F2cTxRiMbvTczMDT1jpwIigZHsZSdqiy4u", + "statsig-sdk-version": "5.1.0", + "statsig-client-time": int(time.time() * 1000) + }) token = headers.get("authorization", "").replace("Bearer ", "") if token: @@ -117,28 +194,39 @@ async def chatgpt_reverse_proxy(request: Request, path: str): data = await request.body() - client = Client(proxy=random.choice(proxy_url_list) if proxy_url_list else None) + client = Client(proxy=proxy_url, impersonate=impersonate) try: background = BackgroundTask(client.close) r = await client.request(request.method, f"{base_url}/{path}", params=params, headers=headers, cookies=request_cookies, data=data, stream=True, allow_redirects=False) - + if r.status_code == 307: + if "bing" in path: + return Response(status_code=307, + headers={"Location": r.headers.get("Location").replace("chatgpt.com", origin_host) + .replace("https", petrol)}, background=background) if r.status_code == 302: return Response(status_code=302, headers={"Location": r.headers.get("Location").replace("chatgpt.com", origin_host) .replace("cdn.oaistatic.com", origin_host) .replace("https", petrol)}, background=background) elif 'stream' in r.headers.get("content-type", ""): - return StreamingResponse(r.aiter_content(), media_type=r.headers.get("content-type", ""), + logger.info(f"Request token: {req_token}") + logger.info(f"Request proxy: {proxy_url}") + logger.info(f"Request UA: {user_agent}") + logger.info(f"Request impersonate: {impersonate}") + return StreamingResponse(content_generator(r, token), media_type=r.headers.get("content-type", ""), background=background) else: if "/backend-api/conversation" in path or "/register-websocket" in path: response = Response(content=(await r.atext()), media_type=r.headers.get("content-type"), status_code=r.status_code, background=background) else: - content = ((await r.atext()).replace("chatgpt.com", origin_host) + content = await r.atext() + content = (content + .replace("ab.chatgpt.com", origin_host) .replace("cdn.oaistatic.com", origin_host) # .replace("files.oaiusercontent.com", origin_host) + .replace("chatgpt.com", origin_host) .replace("https", petrol)) rheaders = dict(r.headers) content_type = rheaders.get("content-type", "") diff --git a/gateway/share.py b/gateway/share.py new file mode 100644 index 0000000..5b1f10d --- /dev/null +++ b/gateway/share.py @@ -0,0 +1,247 @@ +import json +import random +import uuid + +from fastapi import Request, HTTPException +from fastapi.responses import Response +from starlette.background import BackgroundTask +from starlette.concurrency import run_in_threadpool +from starlette.responses import StreamingResponse + +from app import app +from chatgpt.authorization import get_fp, verify_token +from chatgpt.proofofWork import get_config, get_requirements_token, get_answer_token +from gateway.reverseProxy import get_real_req_token, content_generator +from utils.Client import Client +from utils.Logger import logger +from utils.configs import proxy_url_list, chatgpt_base_url_list, turnstile_solver_url, x_sign, no_sentinel + +base_headers = { + 'accept': '*/*', + 'accept-encoding': 'gzip, deflate, br, zstd', + 'accept-language': 'en-US,en;q=0.9', + 'content-type': 'application/json', + 'oai-language': 'en-US', + 'priority': 'u=1, i', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', +} + + +async def chatgpt_account_check(access_token): + auth_info = {} + client = Client(proxy=random.choice(proxy_url_list) if proxy_url_list else None) + try: + proxy_url = random.choice(proxy_url_list) if proxy_url_list else None + host_url = random.choice(chatgpt_base_url_list) if chatgpt_base_url_list else "https://chatgpt.com" + req_token = await get_real_req_token(access_token) + access_token = await verify_token(req_token) + fp = get_fp(req_token) + + headers = base_headers.copy() + headers.update({"authorization": f"Bearer {access_token}"}) + headers.update(fp) + + client = Client(proxy=proxy_url, impersonate=fp.get("impersonate", "safari15_3")) + r = await client.get(f"{host_url}/backend-api/models?history_and_training_disabled=false", headers=headers, + timeout=10) + if r.status_code != 200: + raise HTTPException(status_code=r.status_code, detail=r.text) + models = r.json() + r = await client.get(f"{host_url}/backend-api/accounts/check/v4-2023-04-27", headers=headers, timeout=10) + if r.status_code != 200: + raise HTTPException(status_code=r.status_code, detail=r.text) + accounts_info = r.json() + + auth_info.update({"models": models["models"]}) + auth_info.update({"accounts_info": accounts_info}) + + account_ordering = accounts_info.get("account_ordering", []) + is_deactivated = None + plan_type = None + team_ids = [] + for account in account_ordering: + this_is_deactivated = accounts_info['accounts'].get(account, {}).get("account", {}).get("is_deactivated", + False) + this_plan_type = accounts_info['accounts'].get(account, {}).get("account", {}).get("plan_type", "free") + + if this_is_deactivated and is_deactivated is None: + is_deactivated = True + else: + is_deactivated = False + + if "team" in this_plan_type: + plan_type = this_plan_type + team_ids.append(account) + elif plan_type is None: + plan_type = this_plan_type + + auth_info.update({"accountCheckInfo": { + "is_deactivated": is_deactivated, + "plan_type": plan_type, + "team_ids": team_ids + }}) + + return auth_info + except Exception as e: + logger.error(f"chatgpt_account_check: {e}") + return {} + finally: + await client.close() + + +async def chatgpt_refresh(refresh_token): + client = Client(proxy=random.choice(proxy_url_list) if proxy_url_list else None) + try: + data = { + "client_id": "pdlLIX2Y72MIl2rhLhTE9VV9bN905kBh", + "grant_type": "refresh_token", + "redirect_uri": "com.openai.chat://auth0.openai.com/ios/com.openai.chat/callback", + "refresh_token": refresh_token + } + r = await client.post("https://auth0.openai.com/oauth/token", json=data, timeout=10) + if r.status_code != 200: + raise HTTPException(status_code=r.status_code, detail=r.text) + res = r.json() + auth_info = {} + auth_info.update(res) + auth_info.update({"refresh_token": refresh_token}) + auth_info.update({"accessToken": res.get("access_token", "")}) + return auth_info + except Exception as e: + logger.error(f"chatgpt_refresh: {e}") + return {} + finally: + await client.close() + + +@app.post("/auth/refresh") +async def refresh(request: Request): + auth_info = {} + form_data = await request.form() + + auth_info.update(form_data) + + access_token = auth_info.get("access_token", auth_info.get("accessToken", "")) + refresh_token = auth_info.get("refresh_token", "") + + if not refresh_token and not access_token: + raise HTTPException(status_code=401, detail="refresh_token or access_token is required") + + if access_token: + account_check_info = await chatgpt_account_check(access_token) + if account_check_info: + auth_info.update(account_check_info) + auth_info.update({"accessToken": access_token}) + return Response(content=json.dumps(auth_info), media_type="application/json") + + if refresh_token: + chatgpt_refresh_info = await chatgpt_refresh(refresh_token) + if chatgpt_refresh_info: + auth_info.update(chatgpt_refresh_info) + access_token = auth_info.get("accessToken", "") + account_check_info = await chatgpt_account_check(access_token) + if account_check_info: + auth_info.update(account_check_info) + auth_info.update({"accessToken": access_token}) + return Response(content=json.dumps(auth_info), media_type="application/json") + raise HTTPException(status_code=401, detail="Unauthorized") + + +if no_sentinel: + @app.post("/backend-api/sentinel/chat-requirements") + async def sentinel_chat_conversations(): + return { + "arkose": { + "dx": None, + "required": False + }, + "persona": "chatgpt-paid", + "proofofwork": { + "difficulty": None, + "required": False, + "seed": None + }, + "token": str(uuid.uuid4()), + "turnstile": { + "dx": None, + "required": False + } + } + + + @app.post("/backend-api/conversation") + async def chat_conversations(request: Request): + token = request.headers.get("Authorization", "").replace("Bearer ", "") + req_token = await get_real_req_token(token) + access_token = await verify_token(req_token) + fp = get_fp(req_token) + proxy_url = fp.get("proxy_url", None) + user_agent = fp.get("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0") + impersonate = fp.get("impersonate", "safari15_3") + + host_url = random.choice(chatgpt_base_url_list) if chatgpt_base_url_list else "https://chatgpt.com" + proof_token = None + turnstile_token = None + + headers = base_headers.copy() + headers.update(fp) + headers.update({"authorization": f"Bearer {access_token}"}) + + client = Client(proxy=proxy_url, impersonate=impersonate) + + config = get_config(user_agent) + p = get_requirements_token(config) + data = {'p': p} + r = await client.post(f'{host_url}/backend-api/sentinel/chat-requirements', headers=headers, json=data, + timeout=10) + resp = r.json() + turnstile = resp.get('turnstile', {}) + turnstile_required = turnstile.get('required') + if turnstile_required: + turnstile_dx = turnstile.get("dx") + try: + if turnstile_solver_url: + res = await client.post(turnstile_solver_url, + json={"url": "https://chatgpt.com", "p": p, "dx": turnstile_dx}) + turnstile_token = res.json().get("t") + except Exception as e: + logger.info(f"Turnstile ignored: {e}") + + proofofwork = resp.get('proofofwork', {}) + proofofwork_required = proofofwork.get('required') + if proofofwork_required: + proofofwork_diff = proofofwork.get("difficulty") + proofofwork_seed = proofofwork.get("seed") + proof_token, solved = await run_in_threadpool( + get_answer_token, proofofwork_seed, proofofwork_diff, config + ) + if not solved: + raise HTTPException(status_code=403, detail="Failed to solve proof of work") + chat_token = resp.get('token') + headers.update({ + "openai-sentinel-chat-requirements-token": chat_token, + "openai-sentinel-proof-token": proof_token, + "openai-sentinel-turnstile-token": turnstile_token, + }) + + params = dict(request.query_params) + data = await request.body() + request_cookies = dict(request.cookies) + background = BackgroundTask(client.close) + r = await client.post_stream(f"{host_url}/backend-api/conversation", params=params, headers=headers, + cookies=request_cookies, data=data, stream=True, allow_redirects=False) + rheaders = r.headers + if x_sign: + rheaders.update({"x-sign": x_sign}) + if 'stream' in rheaders.get("content-type", ""): + logger.info(f"Request token: {req_token}") + logger.info(f"Request proxy: {proxy_url}") + logger.info(f"Request UA: {user_agent}") + logger.info(f"Request impersonate: {impersonate}") + return StreamingResponse(content_generator(r, token), headers=rheaders, + media_type=rheaders.get("content-type"), background=background) + else: + return Response(content=(await r.atext()), headers=rheaders, media_type=rheaders.get("content-type"), + status_code=r.status_code, background=background) diff --git a/templates/chatgpt.html b/templates/chatgpt.html index b1d6f26..6447bca 100644 --- a/templates/chatgpt.html +++ b/templates/chatgpt.html @@ -1,5 +1,5 @@ - + @@ -18,54 +18,55 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -202,7 +203,7 @@
- - diff --git a/templates/login.html b/templates/login.html index 2ad8dd3..5f16ca8 100644 --- a/templates/login.html +++ b/templates/login.html @@ -34,7 +34,10 @@