137 lines
5.7 KiB
Python
137 lines
5.7 KiB
Python
import os
|
|
import requests
|
|
import logging
|
|
import re
|
|
from urllib.parse import urlparse, urljoin
|
|
from bs4 import BeautifulSoup
|
|
from flask import Flask, Response, request, stream_with_context
|
|
from dotenv import load_dotenv
|
|
from waitress import serve
|
|
|
|
# Load environment variables from .env file
|
|
load_dotenv()
|
|
|
|
app = Flask(__name__)
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
# Get target URL from environment variables
|
|
TARGET_URL = os.environ.get('TARGET_URL')
|
|
if not TARGET_URL:
|
|
raise ValueError("TARGET_URL is not set in the .env file")
|
|
|
|
TARGET_PARSED = urlparse(TARGET_URL)
|
|
TARGET_HOST = TARGET_PARSED.netloc
|
|
|
|
def rewrite_text(content, content_type, proxy_base_url):
|
|
"""Rewrites URLs in text-based content (HTML, CSS, JS)."""
|
|
if not content:
|
|
return content
|
|
|
|
soup = None
|
|
if 'text/html' in content_type:
|
|
soup = BeautifulSoup(content, 'lxml')
|
|
# Rewrite links in tags like a, link, script, img, etc.
|
|
for tag, attr in [('a', 'href'), ('link', 'href'), ('script', 'src'), ('img', 'src'), ('form', 'action')]:
|
|
for t in soup.find_all(tag, **{attr: True}):
|
|
original_url = t[attr]
|
|
absolute_url = urljoin(TARGET_URL, original_url)
|
|
if urlparse(absolute_url).netloc == TARGET_HOST:
|
|
t[attr] = absolute_url.replace(TARGET_URL, proxy_base_url, 1)
|
|
|
|
# Rewrite inline styles
|
|
for tag in soup.find_all(style=True):
|
|
tag['style'] = re.sub(r'url\((.*?)\)',
|
|
lambda m: f"url({urljoin(TARGET_URL, m.group(1)).replace(TARGET_URL, proxy_base_url, 1)})",
|
|
tag['style'])
|
|
|
|
return str(soup)
|
|
|
|
elif 'css' in content_type or 'javascript' in content_type:
|
|
# A simpler regex for CSS/JS url rewriting
|
|
content_str = content.decode('utf-8', errors='ignore')
|
|
# Regex to find url(...) and import statements
|
|
def replace_url(match):
|
|
url = match.group(1) or match.group(2) or match.group(3)
|
|
if url.startswith(('http://', 'https://', '//')):
|
|
return match.group(0) # Don't touch absolute URLs from other domains
|
|
absolute_url = urljoin(TARGET_URL, url)
|
|
return match.group(0).replace(url, absolute_url.replace(TARGET_URL, proxy_base_url, 1))
|
|
|
|
rewritten_content = re.sub(r'url\((["\']?)(.*?)\1\)|@import\s+(["\'])(.*?)\3', replace_url, content_str)
|
|
return rewritten_content.encode('utf-8')
|
|
|
|
return content
|
|
|
|
@app.after_request
|
|
def add_cors_headers(response):
|
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
|
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,PATCH,OPTIONS'
|
|
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization,Origin,Accept,User-Agent,Cookie'
|
|
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
|
return response
|
|
|
|
@app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
|
|
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
|
|
def proxy(path):
|
|
# Construct the full target URL
|
|
target_url = urljoin(TARGET_URL, path)
|
|
if request.query_string:
|
|
target_url += '?' + request.query_string.decode('utf-8')
|
|
|
|
# Prepare headers for the outgoing request
|
|
headers = {key: value for key, value in request.headers if key.lower() != 'host'}
|
|
headers['Host'] = TARGET_HOST
|
|
headers['Referer'] = TARGET_URL
|
|
headers['Origin'] = TARGET_URL.rstrip('/')
|
|
|
|
try:
|
|
resp = requests.request(
|
|
method=request.method,
|
|
url=target_url,
|
|
headers=headers,
|
|
data=request.get_data(),
|
|
cookies=request.cookies,
|
|
stream=True,
|
|
allow_redirects=False, # We handle redirects manually
|
|
timeout=30
|
|
)
|
|
except requests.exceptions.RequestException as e:
|
|
logging.error(f"Proxy error connecting to {target_url}: {e}")
|
|
return "Proxy connection error", 502
|
|
|
|
# Handle redirects
|
|
if resp.status_code in (301, 302, 307, 308):
|
|
location = resp.headers.get('Location')
|
|
if location:
|
|
# Rewrite the redirect location to point back to the proxy
|
|
proxy_location = location.replace(TARGET_URL, request.host_url, 1)
|
|
response = Response(status=resp.status_code)
|
|
response.headers['Location'] = proxy_location
|
|
return response
|
|
|
|
# Filter out problematic headers from the response
|
|
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
|
|
response_headers = [(name, value) for name, value in resp.headers.items() if name.lower() not in excluded_headers]
|
|
|
|
content_type = resp.headers.get('Content-Type', '')
|
|
proxy_base_url = request.host_url
|
|
|
|
def generate():
|
|
# For text content, we need to buffer it, rewrite it, and then send it.
|
|
if any(t in content_type for t in ['html', 'css', 'javascript']):
|
|
content = resp.content # This reads the whole content
|
|
rewritten_content = rewrite_text(content, content_type, proxy_base_url)
|
|
if rewritten_content:
|
|
yield rewritten_content if isinstance(rewritten_content, bytes) else rewritten_content.encode('utf-8')
|
|
else:
|
|
# For other content (images, etc.), stream it directly
|
|
for chunk in resp.iter_content(chunk_size=8192):
|
|
yield chunk
|
|
|
|
response = Response(stream_with_context(generate()), status=resp.status_code, headers=response_headers)
|
|
return response
|
|
|
|
if __name__ == '__main__':
|
|
print("Starting production server on http://0.0.0.0:8080")
|
|
serve(app, host='0.0.0.0', port=8080, threads=16)
|