rkihacker commited on
Commit
02594ce
·
verified ·
1 Parent(s): a42e3f7

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +70 -27
main.py CHANGED
@@ -2,9 +2,8 @@ import httpx
2
  from fastapi import FastAPI, Request, HTTPException
3
  from starlette.responses import StreamingResponse, JSONResponse
4
  from starlette.background import BackgroundTask
5
- from pydantic_settings import BaseSettings
6
  from pydantic import Field, field_validator
7
- from typing import Set
8
  from contextlib import asynccontextmanager
9
  import os
10
  import random
@@ -15,6 +14,18 @@ import uuid
15
  from faker import Faker
16
  from fake_useragent import UserAgent
17
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  # --- Structured Configuration using Pydantic ---
19
  class Settings(BaseSettings):
20
  """Manages application settings and configuration from environment variables."""
@@ -22,11 +33,10 @@ class Settings(BaseSettings):
22
  TARGET_URL: str = Field(default="https://api.gmi-serving.com")
23
  MAX_RETRIES: int = Field(default=5, gt=0)
24
 
25
- # Retry logic settings
26
  RETRY_CODES_STR: str = Field(default="429,500,502,503,504", alias="RETRY_CODES")
27
  RETRY_STATUS_CODES: Set[int] = {429, 500, 502, 503, 504}
28
- BACKOFF_FACTOR: float = Field(default=0.5, gt=0) # Base delay for backoff in seconds
29
- JITTER_FACTOR: float = Field(default=0.2, ge=0) # Randomness factor for backoff
30
 
31
  @field_validator('RETRY_STATUS_CODES', mode='before')
32
  @classmethod
@@ -45,22 +55,28 @@ class Settings(BaseSettings):
45
 
46
  # --- Initialize Settings and Global Services ---
47
  settings = Settings()
48
- logging.basicConfig(
49
- level=settings.LOG_LEVEL.upper(),
50
- format='%(asctime)s - %(levelname)s - [%(request_id)s] - %(message)s'
51
- )
52
- # Add a filter to include request_id in logs
53
  class RequestIdFilter(logging.Filter):
54
  def filter(self, record):
55
  record.request_id = getattr(record, 'request_id', 'main')
56
  return True
57
 
 
 
 
 
58
  logger = logging.getLogger(__name__)
59
  logger.addFilter(RequestIdFilter())
60
 
61
  # Initialize data generation tools
62
  faker = Faker()
63
- ua = UserAgent()
 
 
 
 
 
64
 
65
  # --- HTTPX Client Lifecycle Management ---
66
  @asynccontextmanager
@@ -77,26 +93,58 @@ async def lifespan(app: FastAPI):
77
  app = FastAPI(docs_url=None, redoc_url=None, lifespan=lifespan)
78
 
79
  # --- Helper Functions ---
80
- def prepare_forward_headers(incoming_headers: dict, client_host: str) -> dict:
81
- """Prepares headers for the downstream request, adding spoofed IP and User-Agent."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  forward_headers = {k.lower(): v for k, v in incoming_headers.items()}
83
 
84
- # Remove headers that are specific to the incoming request's connection
85
- forward_headers.pop("host", None)
86
- forward_headers.pop("content-length", None)
 
 
 
 
87
 
88
- # Generate realistic fake data
89
  spoofed_ip = faker.ipv4_public()
90
- user_agent = ua.random
91
 
92
- # Add/overwrite headers to obfuscate the origin
93
  override_headers = {
94
- "user-agent": user_agent,
95
- "x-forwarded-for": f"{spoofed_ip}, {client_host}",
96
  "x-real-ip": spoofed_ip,
 
 
 
 
 
 
97
  "x-originating-ip": spoofed_ip,
98
  "x-remote-ip": spoofed_ip,
99
- "x-remote-addr": spoofed_ip
 
 
 
 
 
 
 
 
 
 
100
  }
101
  forward_headers.update(override_headers)
102
 
@@ -134,7 +182,6 @@ async def reverse_proxy_handler(request: Request):
134
  last_exception = None
135
  for attempt in range(settings.MAX_RETRIES):
136
  if attempt > 0:
137
- # Exponential backoff with jitter
138
  backoff_delay = settings.BACKOFF_FACTOR * (2 ** (attempt - 1))
139
  jitter = random.uniform(-settings.JITTER_FACTOR, settings.JITTER_FACTOR) * backoff_delay
140
  sleep_duration = max(0, backoff_delay + jitter)
@@ -150,7 +197,6 @@ async def reverse_proxy_handler(request: Request):
150
  )
151
  resp = await client.send(req, stream=True)
152
 
153
- # If status is not in retry codes, or it's the last attempt, stream the response back
154
  if resp.status_code not in settings.RETRY_STATUS_CODES or attempt == settings.MAX_RETRIES - 1:
155
  duration_ms = (time.monotonic() - start_time) * 1000
156
  log_func = logger.info if resp.is_success else logger.warning
@@ -165,18 +211,15 @@ async def reverse_proxy_handler(request: Request):
165
  background=BackgroundTask(resp.aclose),
166
  )
167
 
168
- # If we are going to retry, close the current response to free the connection
169
  await resp.aclose()
170
  last_exception = f"Last failed attempt returned status code {resp.status_code}"
171
 
172
  except httpx.RequestError as e:
173
  last_exception = e
174
  logger.warning(f"HTTPX RequestError on attempt {attempt + 1}: {e}", extra=log_extra)
175
- # If it's the last attempt, we will fall through and raise a 502
176
  if attempt == settings.MAX_RETRIES - 1:
177
  break
178
 
179
- # This part is reached only if all retries fail
180
  duration_ms = (time.monotonic() - start_time) * 1000
181
  logger.critical(
182
  f"Failed permanently: {request.method} {url.path} after {settings.MAX_RETRIES} attempts. latency={duration_ms:.2f}ms. Last error: {last_exception}",
 
2
  from fastapi import FastAPI, Request, HTTPException
3
  from starlette.responses import StreamingResponse, JSONResponse
4
  from starlette.background import BackgroundTask
 
5
  from pydantic import Field, field_validator
6
+ from typing import Set, Dict
7
  from contextlib import asynccontextmanager
8
  import os
9
  import random
 
14
  from faker import Faker
15
  from fake_useragent import UserAgent
16
 
17
+ # --- Pydantic V1/V2 Compatibility ---
18
+ # This block makes the code resilient to different Pydantic versions.
19
+ try:
20
+ # Recommended for Pydantic V2
21
+ from pydantic_settings import BaseSettings
22
+ print("Using pydantic_settings.BaseSettings")
23
+ except ImportError:
24
+ # Fallback for Pydantic V1
25
+ from pydantic import BaseSettings
26
+ print("pydantic_settings not found, falling back to pydantic.BaseSettings")
27
+
28
+
29
  # --- Structured Configuration using Pydantic ---
30
  class Settings(BaseSettings):
31
  """Manages application settings and configuration from environment variables."""
 
33
  TARGET_URL: str = Field(default="https://api.gmi-serving.com")
34
  MAX_RETRIES: int = Field(default=5, gt=0)
35
 
 
36
  RETRY_CODES_STR: str = Field(default="429,500,502,503,504", alias="RETRY_CODES")
37
  RETRY_STATUS_CODES: Set[int] = {429, 500, 502, 503, 504}
38
+ BACKOFF_FACTOR: float = Field(default=0.5, gt=0)
39
+ JITTER_FACTOR: float = Field(default=0.2, ge=0)
40
 
41
  @field_validator('RETRY_STATUS_CODES', mode='before')
42
  @classmethod
 
55
 
56
  # --- Initialize Settings and Global Services ---
57
  settings = Settings()
58
+
59
+ # Custom logging filter to inject request_id
 
 
 
60
  class RequestIdFilter(logging.Filter):
61
  def filter(self, record):
62
  record.request_id = getattr(record, 'request_id', 'main')
63
  return True
64
 
65
+ logging.basicConfig(
66
+ level=settings.LOG_LEVEL.upper(),
67
+ format='%(asctime)s - %(levelname)s - [%(request_id)s] - %(message)s'
68
+ )
69
  logger = logging.getLogger(__name__)
70
  logger.addFilter(RequestIdFilter())
71
 
72
  # Initialize data generation tools
73
  faker = Faker()
74
+ try:
75
+ ua = UserAgent()
76
+ except Exception:
77
+ # Fallback if fake-useragent server is down
78
+ ua = None
79
+ logger.warning("Could not initialize UserAgent. A fallback will be used.")
80
 
81
  # --- HTTPX Client Lifecycle Management ---
82
  @asynccontextmanager
 
93
  app = FastAPI(docs_url=None, redoc_url=None, lifespan=lifespan)
94
 
95
  # --- Helper Functions ---
96
+ def get_random_user_agent() -> str:
97
+ """Returns a random User-Agent, with a fallback."""
98
+ if ua:
99
+ try:
100
+ return ua.random
101
+ except Exception:
102
+ logger.warning("Failed to get random User-Agent, using fallback.")
103
+ # Fallback User-Agent
104
+ return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
105
+
106
+ def prepare_forward_headers(incoming_headers: Dict, client_host: str) -> (Dict, str):
107
+ """
108
+ Prepares headers for the downstream request, adding a comprehensive set of
109
+ spoofed IP and a random User-Agent.
110
+ """
111
+ # Start with a clean slate of lower-cased headers
112
  forward_headers = {k.lower(): v for k, v in incoming_headers.items()}
113
 
114
+ # Remove headers that are specific to the incoming request's connection or could leak info
115
+ headers_to_remove = [
116
+ "host", "content-length", "x-forwarded-for", "x-real-ip", "forwarded",
117
+ "via", "x-client-ip", "x-forwarded-proto", "x-forwarded-host"
118
+ ]
119
+ for h in headers_to_remove:
120
+ forward_headers.pop(h, None)
121
 
 
122
  spoofed_ip = faker.ipv4_public()
 
123
 
124
+ # Add a comprehensive set of headers to mask the origin
125
  override_headers = {
126
+ # Standard headers
127
+ "x-forwarded-for": f"{spoofed_ip}, {faker.ipv4_public()}", # Append a fake proxy chain
128
  "x-real-ip": spoofed_ip,
129
+
130
+ # RFC 7239 standard, more structured
131
+ "forwarded": f"for={spoofed_ip};proto=https",
132
+
133
+ # Common non-standard headers
134
+ "x-client-ip": spoofed_ip,
135
  "x-originating-ip": spoofed_ip,
136
  "x-remote-ip": spoofed_ip,
137
+ "x-remote-addr": spoofed_ip,
138
+
139
+ # Cloudflare-specific headers
140
+ "cf-connecting-ip": spoofed_ip,
141
+ "true-client-ip": spoofed_ip,
142
+
143
+ # Other proxy headers
144
+ "via": "1.1 google", # Fake a passthrough via a common service
145
+
146
+ # Dynamic User-Agent
147
+ "user-agent": get_random_user_agent(),
148
  }
149
  forward_headers.update(override_headers)
150
 
 
182
  last_exception = None
183
  for attempt in range(settings.MAX_RETRIES):
184
  if attempt > 0:
 
185
  backoff_delay = settings.BACKOFF_FACTOR * (2 ** (attempt - 1))
186
  jitter = random.uniform(-settings.JITTER_FACTOR, settings.JITTER_FACTOR) * backoff_delay
187
  sleep_duration = max(0, backoff_delay + jitter)
 
197
  )
198
  resp = await client.send(req, stream=True)
199
 
 
200
  if resp.status_code not in settings.RETRY_STATUS_CODES or attempt == settings.MAX_RETRIES - 1:
201
  duration_ms = (time.monotonic() - start_time) * 1000
202
  log_func = logger.info if resp.is_success else logger.warning
 
211
  background=BackgroundTask(resp.aclose),
212
  )
213
 
 
214
  await resp.aclose()
215
  last_exception = f"Last failed attempt returned status code {resp.status_code}"
216
 
217
  except httpx.RequestError as e:
218
  last_exception = e
219
  logger.warning(f"HTTPX RequestError on attempt {attempt + 1}: {e}", extra=log_extra)
 
220
  if attempt == settings.MAX_RETRIES - 1:
221
  break
222
 
 
223
  duration_ms = (time.monotonic() - start_time) * 1000
224
  logger.critical(
225
  f"Failed permanently: {request.method} {url.path} after {settings.MAX_RETRIES} attempts. latency={duration_ms:.2f}ms. Last error: {last_exception}",