working proxy without any features

This commit is contained in:
Mathieu Broillet 2025-08-24 16:02:31 +02:00
commit 44157290cc
Signed by: mathieub
GPG Key ID: 4428608CDA3A98D3
3 changed files with 3141 additions and 0 deletions

55
client.py Normal file
View File

@ -0,0 +1,55 @@
import subprocess
from httpx import Request, Response, HTTPTransport, AsyncHTTPTransport
def resolve_host(host, dns_server="8.8.8.8") -> list[str]:
try:
result = subprocess.run(
["dig", "+short", host, f"@{dns_server}"],
capture_output=True,
text=True,
check=True
)
ips = result.stdout.strip().split("\n")
return [ip for ip in ips if ip]
except subprocess.CalledProcessError:
return []
class NameSolver:
def get(self, name: str) -> str:
if name == 'clients.plex.tv':
hosts = resolve_host(name)
if hosts:
return hosts[0]
return ''
def resolve(self, request: Request) -> Request:
host = request.url.host
ip = self.get(host)
if ip:
request.extensions["sni_hostname"] = host
request.url = request.url.copy_with(host=ip)
return request
class CustomHost(HTTPTransport):
def __init__(self, solver: NameSolver, *args, **kwargs) -> None:
self.solver = solver
super().__init__(*args, **kwargs)
def handle_request(self, request: Request) -> Response:
request = self.solver.resolve(request)
return super().handle_request(request)
class AsyncCustomHost(AsyncHTTPTransport):
def __init__(self, solver: NameSolver, *args, **kwargs) -> None:
self.solver = solver
super().__init__(*args, **kwargs)
async def handle_async_request(self, request: Request) -> Response:
request = self.solver.resolve(request)
return await super().handle_async_request(request)

3003
const.py Normal file

File diff suppressed because it is too large Load Diff

83
main_app.py Normal file
View File

@ -0,0 +1,83 @@
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import Response
from starlette.responses import JSONResponse
from client import AsyncCustomHost, NameSolver
from const import OFFICIAL_API
app = FastAPI()
# Configure upstream client
official_client = httpx.AsyncClient(
verify=True,
timeout=httpx.Timeout(30.0, connect=10.0),
limits=httpx.Limits(max_keepalive_connections=50, max_connections=100),
transport=AsyncCustomHost(NameSolver())
)
# Hop-by-hop headers we must strip
HOP_BY_HOP_HEADERS = {
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
"content-length", # let httpx calculate
"host", # must be rewritten
}
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def catch_all(request: Request, path: str):
# Copy request body
body = await request.body()
# Copy headers, filtering hop-by-hop
req_headers = {
k: v
for k, v in request.headers.items()
if k.lower() not in HOP_BY_HOP_HEADERS
}
# Rewrite Host header to match upstream
req_headers["host"] = httpx.URL(OFFICIAL_API).host
# Forward request upstream
upstream_response = await official_client.request(
method=request.method,
url=f"{OFFICIAL_API}/{path}",
headers=req_headers,
content=body,
params=request.query_params,
cookies=request.cookies,
)
# Copy response headers, filtering hop-by-hop
resp_headers = {
k: v
for k, v in upstream_response.headers.items()
if k.lower() not in HOP_BY_HOP_HEADERS
}
content_type = upstream_response.headers.get("content-type", "")
resp_headers.pop("content-encoding", None) # remove, since body is decoded
if content_type.startswith("application/json"):
response = JSONResponse(
content=upstream_response.json(),
status_code=upstream_response.status_code,
headers=resp_headers
)
else:
response = Response(
content=upstream_response.content,
status_code=upstream_response.status_code,
headers=resp_headers,
media_type=content_type
)
return response