working proxy without any features
This commit is contained in:
commit
44157290cc
55
client.py
Normal file
55
client.py
Normal 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)
|
83
main_app.py
Normal file
83
main_app.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user