rkihacker commited on
Commit
063d7d5
·
verified ·
1 Parent(s): e50ca24

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +11 -43
  2. main.py +92 -102
  3. requirements.txt +0 -2
Dockerfile CHANGED
@@ -1,53 +1,21 @@
1
- # --- Stage 1: Build Dependencies ---
2
- FROM python:3.9-slim as builder
3
-
4
- # Set environment variables to prevent writing .pyc files and for unbuffered output
5
- ENV PYTHONDONTWRITEBYTECODE 1
6
- ENV PYTHONUNBUFFERED 1
7
-
8
- # Set working directory
9
- WORKDIR /app
10
-
11
- # Install uvloop and gunicorn first as they are core dependencies
12
- RUN pip install --no-cache-dir uvloop gunicorn
13
-
14
- # Copy requirements and install the rest of the packages
15
- COPY requirements.txt .
16
- RUN pip install --no-cache-dir -r requirements.txt
17
-
18
-
19
- # --- Stage 2: Final Production Image ---
20
  FROM python:3.9-slim
21
 
22
- # Set the working directory
23
  WORKDIR /app
24
 
25
- # Set same environment variables for consistency
26
- ENV PYTHONDONTWRITEBYTECODE 1
27
- ENV PYTHONUNBUFFERED 1
28
-
29
- # Create a non-root user and group for security
30
- # This is a more robust way to create a user with a home directory
31
- RUN addgroup --system app && adduser --system --ingroup app --shell /bin/sh --home /app app
32
-
33
- # Copy installed packages AND binaries from the builder stage
34
- # This is the CRUCIAL FIX: copying /usr/local/bin where gunicorn lives
35
- COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
36
- COPY --from=builder /usr/local/bin /usr/local/bin
37
-
38
- # Copy the application code
39
- COPY . .
40
 
41
- # Change ownership of the app directory to the non-root user
42
- # This ensures the user can read the files
43
- RUN chown -R app:app /app
44
 
45
- # Switch to the non-root user
46
- USER app
47
 
48
  # Expose the port the app runs on
49
  EXPOSE 8000
50
 
51
- # Run the application using Gunicorn
52
- # The command is now guaranteed to be in the PATH
53
- CMD ["gunicorn", "-c", "gunicorn_conf.py", "main:app"]
 
1
+ # Use an official Python runtime as a parent image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  FROM python:3.9-slim
3
 
4
+ # Set the working directory in the container
5
  WORKDIR /app
6
 
7
+ # Copy the dependencies file to the working directory
8
+ COPY requirements.txt .
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # Install any needed packages specified in requirements.txt
11
+ RUN pip install --no-cache-dir -r requirements.txt
 
12
 
13
+ # Copy the rest of the application's code to the working directory
14
+ COPY main.py .
15
 
16
  # Expose the port the app runs on
17
  EXPOSE 8000
18
 
19
+ # Run the application with Uvicorn
20
+ # The host 0.0.0.0 makes the server accessible from outside the container
21
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
main.py CHANGED
@@ -1,121 +1,111 @@
1
  import httpx
2
  from fastapi import FastAPI, Request, HTTPException
3
- from fastapi.responses import StreamingResponse
4
- import json
5
- import random
6
- import logging
7
- import ipaddress
8
 
9
- # Configure logging
10
- logging.basicConfig(
11
- level=logging.INFO,
12
- format="%(asctime)s - %(levelname)s - %(message)s",
13
- datefmt="%Y-%m-%d %H:%M:%S",
14
- )
15
-
16
- app = FastAPI()
17
 
18
- # List of API URLs to be randomized
19
- API_URLS = [
20
- "https://api.deepinfra.com/v1/openai/chat/completions",
21
- "https://stage.api.deepinfra.com/v1/openai/chat/completions",
22
- ]
 
23
 
24
- # A pool of User-Agents (you can expand this list)
25
- USER_AGENTS = [
26
- # Chrome (Windows)
27
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
28
- # Firefox (Windows)
29
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
30
- # Safari (macOS)
31
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
32
- # Edge (Windows)
33
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
34
- # Chrome (Android)
35
- "Mozilla/5.0 (Linux; Android 14; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36",
36
- # Safari (iOS)
37
- "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
38
- ]
39
 
40
- def generate_random_ip() -> str:
41
- """Generate a random IPv4 address, avoiding reserved ranges."""
42
- while True:
43
- ip = ipaddress.IPv4Address(random.getrandbits(32))
44
- if not (ip.is_private or ip.is_multicast or ip.is_reserved or ip.is_loopback):
45
- return str(ip)
46
 
47
- @app.post("/v1/openai/chat/completions")
48
- async def proxy_deepinfra(request: Request):
49
  """
50
- Proxies chat completion requests to the DeepInfra API.
51
- Randomizes API URLs, spoofed random IP, fake headers, and User-Agent rotation.
52
  """
53
- try:
54
- body = await request.json()
55
- except json.JSONDecodeError:
56
- raise HTTPException(status_code=400, detail="Invalid JSON in request body")
 
 
 
 
57
 
58
- # Random spoofed IP + random User-Agent
59
- random_ip = generate_random_ip()
60
- user_agent = random.choice(USER_AGENTS)
 
 
61
 
62
- headers = {
63
- # Browser/device headers
64
- "User-Agent": user_agent,
65
- "accept": "text/event-stream",
66
- "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
 
 
 
 
 
67
  "sec-ch-ua-mobile": "?0",
68
  "sec-ch-ua-platform": '"Windows"',
69
- "Referer": "https://deepinfra.com/",
70
- "Origin": "https://deepinfra.com",
 
 
 
 
71
 
72
- # Spoofed IP headers
73
- "X-Forwarded-For": random_ip,
74
- "X-Real-IP": random_ip,
75
- "Forwarded": f"for={random_ip};proto=https",
 
 
 
76
 
77
- # Extra fake headers
78
- "DNT": "1",
79
- "Pragma": "no-cache",
80
- "Cache-Control": "no-cache",
81
- "Accept-Encoding": "gzip, deflate, br, zstd",
82
- "Accept-Language": "en-US,en;q=0.9,fr;q=0.8,de;q=0.7",
83
- "Upgrade-Insecure-Requests": "1",
84
- "Sec-Fetch-Dest": "document",
85
- "Sec-Fetch-Mode": "navigate",
86
- "Sec-Fetch-Site": "none",
87
- "Sec-Fetch-User": "?1",
88
 
89
- # Deepinfra-specific
90
- "X-Deepinfra-Source": request.headers.get("X-Deepinfra-Source", "web-embed"),
91
- "Content-Type": "application/json",
92
- }
 
 
 
93
 
94
- shuffled_urls = random.sample(API_URLS, len(API_URLS))
 
 
 
 
 
 
 
 
 
 
95
 
96
- async def stream_generator():
97
- last_error = None
98
- for url in shuffled_urls:
99
- logging.info(
100
- f"Attempting to connect to: {url} with spoofed IP {random_ip} and UA {user_agent}"
101
- )
102
- try:
103
- async with httpx.AsyncClient() as client:
104
- async with client.stream(
105
- "POST", url, headers=headers, json=body, timeout=None
106
- ) as response:
107
- response.raise_for_status()
108
- logging.info(f"Successfully connected. Streaming from: {url}")
109
- async for chunk in response.aiter_bytes():
110
- yield chunk
111
- return
112
- except (httpx.RequestError, httpx.HTTPStatusError) as e:
113
- last_error = e
114
- logging.warning(
115
- f"Failed to connect to {url}: {e}. Trying next URL."
116
- )
117
- continue
118
- if last_error:
119
- logging.error(f"All API endpoints failed. Last error: {last_error}")
120
 
121
- return StreamingResponse(stream_generator(), media_type="text-event-stream")
 
1
  import httpx
2
  from fastapi import FastAPI, Request, HTTPException
3
+ from starlette.responses import StreamingResponse
4
+ from starlette.background import BackgroundTask
5
+ import os
6
+ from contextlib import asynccontextmanager
 
7
 
8
+ # --- Configuration ---
9
+ # The target URL is configurable via an environment variable.
10
+ TARGET_URL = os.getenv("TARGET_URL", "https://console.gmicloud.ai")
 
 
 
 
 
11
 
12
+ # --- HTTPX Client Lifecycle Management ---
13
+ @asynccontextmanager
14
+ async def lifespan(app: FastAPI):
15
+ """
16
+ Manages the lifecycle of the HTTPX client.
17
+ The client is created on startup and gracefully closed on shutdown.
18
 
19
+ WARNING: This client has no timeout and no explicit connection pool limits.
20
+ """
21
+ # timeout=None disables all client-side timeouts.
22
+ # The absence of a `limits` parameter means we rely on system defaults.
23
+ async with httpx.AsyncClient(base_url=TARGET_URL, timeout=None) as client:
24
+ app.state.http_client = client
25
+ yield
 
 
 
 
 
 
 
 
26
 
27
+ # Initialize the FastAPI app with the lifespan manager and disable default docs
28
+ app = FastAPI(docs_url=None, redoc_url=None, lifespan=lifespan)
 
 
 
 
29
 
30
+ # --- Reverse Proxy Logic ---
31
+ async def _reverse_proxy(request: Request):
32
  """
33
+ Forwards a request specifically for the /chat endpoint to the target URL.
34
+ It injects required headers and strips any user-provided Authorization header.
35
  """
36
+ client: httpx.AsyncClient = request.app.state.http_client
37
+
38
+ # Construct the URL for the outgoing request using the incoming path and query.
39
+ url = httpx.URL(path=request.url.path, query=request.url.query.encode("utf-8"))
40
+
41
+ # --- Header Processing ---
42
+ # Start with headers from the incoming request.
43
+ request_headers = dict(request.headers)
44
 
45
+ # 1. CRITICAL: Remove host and authorization headers.
46
+ # The 'host' header is managed by httpx.
47
+ # Removing 'authorization' prevents the user's key from reaching the backend.
48
+ request_headers.pop("host", None)
49
+ request_headers.pop("authorization", None)
50
 
51
+ # 2. Set the specific, required headers for the target API.
52
+ # This will overwrite any conflicting headers from the original request.
53
+ specific_headers = {
54
+ "accept": "application/json, text/plain, */*",
55
+ "accept-language": "en-US,en;q=0.9,ru;q=0.8",
56
+ "content-type": "application/json",
57
+ "origin": "https://console.gmicloud.ai",
58
+ "priority": "u=1, i",
59
+ "referer": "https://console.gmicloud.ai/playground/llm/deepseek-r1-0528/01da5dd6-aa6a-40cb-9dbd-241467aa5cbb?tab=playground",
60
+ "sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
61
  "sec-ch-ua-mobile": "?0",
62
  "sec-ch-ua-platform": '"Windows"',
63
+ "sec-fetch-dest": "empty",
64
+ "sec-fetch-mode": "cors",
65
+ "sec-fetch-site": "same-origin",
66
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
67
+ }
68
+ request_headers.update(specific_headers)
69
 
70
+ # Build the final request to the target service.
71
+ rp_req = client.build_request(
72
+ method=request.method,
73
+ url=url,
74
+ headers=request_headers,
75
+ content=await request.body(),
76
+ )
77
 
78
+ try:
79
+ # Send the request and get a streaming response.
80
+ rp_resp = await client.send(rp_req, stream=True)
81
+ except httpx.ConnectError as e:
82
+ # This error occurs if the target service is down or unreachable.
83
+ raise HTTPException(status_code=502, detail=f"Bad Gateway: Cannot connect to target service. {e}")
 
 
 
 
 
84
 
85
+ # Stream the response from the target service back to the original client.
86
+ return StreamingResponse(
87
+ rp_resp.aiter_raw(),
88
+ status_code=rp_resp.status_code,
89
+ headers=rp_resp.headers,
90
+ background=BackgroundTask(rp_resp.aclose),
91
+ )
92
 
93
+ # --- API Endpoint ---
94
+ @app.api_route(
95
+ "/chat",
96
+ methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
97
+ )
98
+ async def chat_proxy_handler(request: Request):
99
+ """
100
+ This endpoint captures requests specifically for the "/chat" path
101
+ and forwards them through the reverse proxy.
102
+ """
103
+ return await _reverse_proxy(request)
104
 
105
+ # A simple root endpoint for health checks.
106
+ @app.get("/")
107
+ async def health_check():
108
+ """Provides a basic health check endpoint."""
109
+ return {"status": "ok", "proxying_endpoint": "/chat", "target": "TypeGPT"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ # Any request to a path other than "/chat" or "/" will result in a 404 Not Found.
requirements.txt CHANGED
@@ -1,5 +1,3 @@
1
  fastapi
2
  uvicorn
3
  httpx
4
- gunicorn
5
- uvloop
 
1
  fastapi
2
  uvicorn
3
  httpx