diff --git a/api/chat2api.py b/api/chat2api.py index 62c965a..c76fa3e 100644 --- a/api/chat2api.py +++ b/api/chat2api.py @@ -2,12 +2,13 @@ import asyncio import types from apscheduler.schedulers.asyncio import AsyncIOScheduler -from fastapi import Request, Depends, HTTPException, Form +from fastapi import Request, HTTPException, Form, Security from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse +from fastapi.security import HTTPAuthorizationCredentials from starlette.background import BackgroundTask import utils.globals as globals -from app import templates, oauth2_scheme, app +from app import app, templates, security_scheme from chatgpt.ChatService import ChatService from chatgpt.authorization import refresh_all_tokens from utils.Logger import logger @@ -49,7 +50,8 @@ async def process(request_data, req_token): @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)): +async def send_conversation(request: Request, credentials: HTTPAuthorizationCredentials = Security(security_scheme)): + req_token = credentials.credentials try: request_data = await request.json() except Exception: @@ -87,7 +89,7 @@ async def upload_post(text: str = Form(...)): 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: + with open(globals.TOKENS_FILE, "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)) @@ -98,7 +100,7 @@ async def upload_post(text: str = Form(...)): async def upload_post(): globals.token_list.clear() globals.error_token_list.clear() - with open("../data/token.txt", "w", encoding="utf-8") as f: + with open(globals.TOKENS_FILE, "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)) @@ -115,7 +117,7 @@ async def error_tokens(): 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: + with open(globals.TOKENS_FILE, "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)) diff --git a/app.py b/app.py index e7d66af..52f3f06 100644 --- a/app.py +++ b/app.py @@ -1,11 +1,13 @@ import warnings import uvicorn -from fastapi import FastAPI -from fastapi.security import OAuth2PasswordBearer +from fastapi import FastAPI, HTTPException +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.middleware.cors import CORSMiddleware from fastapi.templating import Jinja2Templates +from utils.configs import enable_gateway, api_prefix + warnings.filterwarnings("ignore") @@ -15,7 +17,11 @@ 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 -app = FastAPI() +app = FastAPI( + docs_url=f"/{api_prefix}/docs", # 设置 Swagger UI 文档路径 + redoc_url=f"/{api_prefix}/redoc", # 设置 Redoc 文档路径 + openapi_url=f"/{api_prefix}/openapi.json" # 设置 OpenAPI JSON 路径 +) app.add_middleware( CORSMiddleware, @@ -26,12 +32,26 @@ app.add_middleware( ) templates = Jinja2Templates(directory="templates") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) +security_scheme = HTTPBearer() + +from app import app -import gateway.backend -import gateway.share import api.chat2api +if enable_gateway: + import gateway.share + import gateway.login + import gateway.chatgpt + import gateway.gpts + import gateway.admin + import gateway.v1 + import gateway.backend +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") + + 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/chatgpt/authorization.py b/chatgpt/authorization.py index 34c811d..dcc0cd7 100644 --- a/chatgpt/authorization.py +++ b/chatgpt/authorization.py @@ -105,6 +105,9 @@ async def verify_token(req_token): return access_token elif len(req_token) == 45: try: + if req_token in globals.error_token_list: + raise HTTPException(status_code=401, detail="Error RefreshToken") + access_token = await rt2ac(req_token, force_refresh=False) return access_token except HTTPException as e: diff --git a/chatgpt/chatLimit.py b/chatgpt/chatLimit.py index ef6453d..08f8bf2 100644 --- a/chatgpt/chatLimit.py +++ b/chatgpt/chatLimit.py @@ -1,4 +1,3 @@ -import threading import time from datetime import datetime diff --git a/gateway/admin.py b/gateway/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/gateway/backend.py b/gateway/backend.py index cf94c72..30be77d 100644 --- a/gateway/backend.py +++ b/gateway/backend.py @@ -1,150 +1,149 @@ import json +import random import re import time +import uuid from fastapi import Request, HTTPException -from fastapi.responses import HTMLResponse, RedirectResponse, Response +from fastapi.responses import RedirectResponse, StreamingResponse, Response +from starlette.background import BackgroundTask +from starlette.concurrency import run_in_threadpool import utils.globals as globals -from app import app, templates -from gateway.reverseProxy import chatgpt_reverse_proxy -from utils.configs import enable_gateway +from app import app +from chatgpt.authorization import verify_token, get_fp +from chatgpt.proofofWork import get_answer_token, get_config, get_requirements_token +from gateway.chatgpt import chatgpt_html +from gateway.reverseProxy import chatgpt_reverse_proxy, content_generator, get_real_req_token, headers_reject_list +from utils.Client import Client +from utils.Logger import logger +from utils.configs import x_sign, turnstile_solver_url, chatgpt_base_url_list, no_sentinel -with open("templates/remix_context.json", "r", encoding="utf-8") as f: - remix_context = json.load(f) +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}"] -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) +@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 -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 - - @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(): +@app.get("/backend-api/gizmos/bootstrap") +async def get_gizmos_bootstrap(request: Request): + token = request.headers.get("Authorization", "").replace("Bearer ", "") + if len(token) == 45 or token.startswith("eyJhbGciOi"): + return await chatgpt_reverse_proxy(request, "backend-api/gizmos/bootstrap") + else: 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 Response(content=json.dumps(conversations, indent=4), media_type="application/json") - - - @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) - globals.conversation_map[conversation_id]["conversation_template_id"] = conversation_details.get("conversation_template_id", None) - globals.conversation_map[conversation_id]["gizmo_id"] = conversation_details.get("gizmo_id", None) - globals.conversation_map[conversation_id]["async_status"] = conversation_details.get("async_status", None) - 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) +@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)) + is_archived = request.query_params.get("is_archived", None) + 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: + if is_archived == "true": + if conversation.get("is_archived", False): + items.append(conversation) 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 + if not conversation.get("is_archived", False): + 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 Response(content=json.dumps(conversations, indent=4), media_type="application/json") - @app.get("/backend-api/me") - async def get_me(request: Request): +@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) + globals.conversation_map[conversation_id]["conversation_template_id"] = conversation_details.get( + "conversation_template_id", None) + globals.conversation_map[conversation_id]["gizmo_id"] = conversation_details.get("gizmo_id", None) + globals.conversation_map[conversation_id]["async_status"] = conversation_details.get("async_status", + None) + 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): + token = request.headers.get("Authorization", "").replace("Bearer ", "") + if len(token) == 45 or token.startswith("eyJhbGciOi"): + return await chatgpt_reverse_proxy(request, "backend-api/me") + else: me = { "object": "user", "id": "org-chatgpt", @@ -188,50 +187,136 @@ if enable_gateway: }, "has_payg_project_spend_limit": True } - return me + return Response(content=json.dumps(me, indent=4), media_type="application/json") - 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.post("/backend-api/edge") +async def edge(): + return Response(status_code=204) - @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 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 + } + } - if re.match("ces/v1", path): - return {"success": True} - if re.match("backend-api/edge", path): - return Response(status_code=204) + @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.pop("proxy_url", None) + impersonate = fp.pop("impersonate", "safari15_3") + 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") - for chatgpt_path in chatgpt_paths: - if re.match(chatgpt_path, path): - return await chatgpt_html(request) + host_url = random.choice(chatgpt_base_url_list) if chatgpt_base_url_list else "https://chatgpt.com" + proof_token = None + turnstile_token = None + headers = { + key: value for key, value in request.headers.items() + if (key.lower() not in ["host", "origin", "referer", "priority", + "oai-device-id"] and key.lower() not in headers_reject_list) + } + headers.update(fp) + headers.update({ + "authorization": f"Bearer {access_token}", + "oai-device-id": fp.get("oai-device-id", str(uuid.uuid4())) + }) + + 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) + + +@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]) +async def reverse_proxy(request: Request, path: str): + token = request.headers.get("Authorization", "").replace("Bearer ", "") + if len(token) != 45 and not token.startswith("eyJhbGciOi"): 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 + for chatgpt_path in chatgpt_paths: + if re.match(chatgpt_path, path): + return await chatgpt_html(request) - 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") + 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) diff --git a/gateway/chatgpt.py b/gateway/chatgpt.py new file mode 100644 index 0000000..ca5fd06 --- /dev/null +++ b/gateway/chatgpt.py @@ -0,0 +1,28 @@ +import json + +from fastapi import Request +from fastapi.responses import HTMLResponse + +from app import app, templates +from gateway.login import login_html +from utils.kv_utils import set_value_for_key + +with open("templates/chatgpt_context.json", "r", encoding="utf-8") as f: + chatgpt_context = json.load(f) + + +@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 = chatgpt_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 diff --git a/gateway/gpts.py b/gateway/gpts.py new file mode 100644 index 0000000..a51a0dc --- /dev/null +++ b/gateway/gpts.py @@ -0,0 +1,24 @@ +import json + +from fastapi import Request +from fastapi.responses import Response + +from app import app +from gateway.chatgpt import chatgpt_html + +with open("templates/gpts_context.json", "r", encoding="utf-8") as f: + gpts_context = json.load(f) + + +@app.get("/gpts") +async def get_gpts(): + return {"kind": "store"} + + +@app.get("/g/g-{gizmo_id}") +async def get_gizmo_json(request: Request, gizmo_id: str): + params = request.query_params + if params.get("_data") == "routes/g.$gizmoId._index": + return Response(content=json.dumps(gpts_context, indent=4), media_type="application/json") + else: + return await chatgpt_html(request) diff --git a/gateway/login.py b/gateway/login.py new file mode 100644 index 0000000..1decce0 --- /dev/null +++ b/gateway/login.py @@ -0,0 +1,10 @@ +from fastapi import Request +from fastapi.responses import HTMLResponse + +from app import app, templates + + +@app.get("/login", response_class=HTMLResponse) +async def login_html(request: Request): + response = templates.TemplateResponse("login.html", {"request": request}) + return response diff --git a/gateway/reverseProxy.py b/gateway/reverseProxy.py index 48f6352..a79052a 100644 --- a/gateway/reverseProxy.py +++ b/gateway/reverseProxy.py @@ -2,25 +2,25 @@ import json import random import time import uuid +from datetime import datetime, timezone 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_fp import utils.globals as globals +from chatgpt.authorization import verify_token, get_req_token, get_fp from utils.Client import Client from utils.Logger import logger -from utils.configs import chatgpt_base_url_list, proxy_url_list +from utils.configs import chatgpt_base_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", "x-forwarded-for", @@ -159,7 +159,8 @@ async def chatgpt_reverse_proxy(request: Request, path: str): headers = { key: value for key, value in request.headers.items() - if (key.lower() not in ["host", "origin", "referer", "priority", "oai-device-id"] and key.lower() not in headers_reject_list) + if (key.lower() not in ["host", "origin", "referer", "priority", + "oai-device-id"] and key.lower() not in headers_reject_list) } base_url = random.choice(chatgpt_base_url_list) if chatgpt_base_url_list else "https://chatgpt.com" @@ -173,9 +174,9 @@ async def chatgpt_reverse_proxy(request: Request, path: str): token = request.cookies.get("token", "") req_token = await get_real_req_token(token) fp = get_fp(req_token) - proxy_url = fp.get("proxy_url") + proxy_url = fp.pop("proxy_url", None) + impersonate = fp.pop("impersonate", "safari15_3") user_agent = fp.get("user-agent") - impersonate = fp.get("impersonate", "safari15_3") headers.update(fp) headers.update({ @@ -185,12 +186,13 @@ async def chatgpt_reverse_proxy(request: Request, path: str): "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) - }) + if "statsig-api-key" not in headers: + 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: @@ -208,14 +210,11 @@ async def chatgpt_reverse_proxy(request: Request, path: str): 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) + if r.status_code == 307 or r.status_code == 302 or r.status_code == 301: + return Response(status_code=307, + headers={"Location": r.headers.get("Location") + .replace("ab.chatgpt.com", origin_host) + .replace("chatgpt.com", origin_host) .replace("cdn.oaistatic.com", origin_host) .replace("https", petrol)}, background=background) elif 'stream' in r.headers.get("content-type", ""): diff --git a/gateway/route.py b/gateway/route.py new file mode 100644 index 0000000..e69de29 diff --git a/gateway/share.py b/gateway/share.py index acd0f35..a54a83a 100644 --- a/gateway/share.py +++ b/gateway/share.py @@ -1,22 +1,19 @@ import json import random -import uuid +import time -from fastapi import Request, HTTPException +import jwt +from fastapi import Request, HTTPException, Security from fastapi.responses import Response -from starlette.background import BackgroundTask -from starlette.concurrency import run_in_threadpool -from starlette.responses import StreamingResponse +from fastapi.security import HTTPAuthorizationCredentials import utils.globals as globals -from app import app +from app import app, security_scheme 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 gateway.reverseProxy import get_real_req_token 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, \ - authorization_list +from utils.configs import proxy_url_list, chatgpt_base_url_list, authorization_list base_headers = { 'accept': '*/*', @@ -31,18 +28,16 @@ base_headers = { } -def verify_authorization(request: Request): - auth_header = request.headers.get("Authorization").replace("Bearer ", "") - - if not auth_header: +def verify_authorization(bearer_token): + if not bearer_token: raise HTTPException(status_code=401, detail="Authorization header is missing") - if auth_header not in authorization_list: + if bearer_token not in authorization_list: raise HTTPException(status_code=401, detail="Invalid authorization") @app.get("/seedtoken") -async def get_seedtoken(request: Request): - verify_authorization(request) +async def get_seedtoken(request: Request, credentials: HTTPAuthorizationCredentials = Security(security_scheme)): + verify_authorization(credentials.credentials) try: params = request.query_params seed = params.get("seed") @@ -69,8 +64,8 @@ async def get_seedtoken(request: Request): @app.post("/seedtoken") -async def set_seedtoken(request: Request): - verify_authorization(request) +async def set_seedtoken(request: Request, credentials: HTTPAuthorizationCredentials = Security(security_scheme)): + verify_authorization(credentials.credentials) data = await request.json() seed = data.get("seed") @@ -91,8 +86,8 @@ async def set_seedtoken(request: Request): @app.delete("/seedtoken") -async def delete_seedtoken(request: Request): - verify_authorization(request) +async def delete_seedtoken(request: Request, credentials: HTTPAuthorizationCredentials = Security(security_scheme)): + verify_authorization(credentials.credentials) try: data = await request.json() @@ -129,17 +124,18 @@ 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) + proxy_url = fp.pop("proxy_url", None) + impersonate = fp.pop("impersonate", "safari15_3") 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")) + client = Client(proxy=proxy_url, impersonate=impersonate) r = await client.get(f"{host_url}/backend-api/models?history_and_training_disabled=false", headers=headers, timeout=10) if r.status_code != 200: @@ -225,14 +221,17 @@ async def refresh(request: Request): if not refresh_token and not access_token: raise HTTPException(status_code=401, detail="refresh_token or access_token is required") + need_refresh = True 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") + try: + access_token_info = jwt.decode(access_token, options={"verify_signature": False}) + exp = access_token_info.get("exp", 0) + if exp > int(time.time()) + 60 * 60 * 24 * 5: + need_refresh = False + except Exception as e: + logger.error(f"access_token: {e}") - if refresh_token: + if refresh_token and need_refresh: chatgpt_refresh_info = await chatgpt_refresh(refresh_token) if chatgpt_refresh_info: auth_info.update(chatgpt_refresh_info) @@ -242,106 +241,13 @@ async def refresh(request: Request): auth_info.update(account_check_info) auth_info.update({"accessToken": access_token}) return Response(content=json.dumps(auth_info), media_type="application/json") + elif 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") + 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}", - "oai-device-id": fp.get("oai-device-id", str(uuid.uuid4())) - }) - - 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/gateway/v1.py b/gateway/v1.py new file mode 100644 index 0000000..fc78129 --- /dev/null +++ b/gateway/v1.py @@ -0,0 +1,28 @@ +import json + +from fastapi import Request +from fastapi.responses import Response + +from app import app +from gateway.reverseProxy import chatgpt_reverse_proxy +from utils.kv_utils import set_value_for_key + + +@app.post("/v1/initialize") +async def initialize(request: Request): + initialize_response = (await chatgpt_reverse_proxy(request, f"/v1/initialize")) + initialize_str = initialize_response.body.decode('utf-8') + initialize_json = json.loads(initialize_str) + set_value_for_key(initialize_json, "ip", "8.8.8.8") + set_value_for_key(initialize_json, "country", "US") + return Response(content=json.dumps(initialize_json, indent=4), media_type="application/json") + + +@app.post("/v1/rgstr") +async def rgstr(): + return Response(status_code=202, content=json.dumps({"success": True}, indent=4), media_type="application/json") + + +@app.post("/ces/v1/{path:path}") +async def ces_v1(): + return Response(status_code=202, content=json.dumps({"success": True}, indent=4), media_type="application/json") diff --git a/requirements.txt b/requirements.txt index 61b5637..dba48a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ pybase64 jinja2 APScheduler ua-generator +pyjwt \ No newline at end of file diff --git a/templates/chatgpt.html b/templates/chatgpt.html index 6447bca..92eed68 100644 --- a/templates/chatgpt.html +++ b/templates/chatgpt.html @@ -1,5 +1,5 @@ - + @@ -18,55 +18,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -96,7 +95,7 @@ }()
-
+
@@ -194,7 +193,7 @@
-