2025-05-09 16:31:14 +01:00
|
|
|
# app.py
|
2025-07-15 15:44:19 +01:00
|
|
|
from flask import (Flask, render_template, request, redirect, url_for, session,
|
|
|
|
send_file, jsonify, send_from_directory, Response)
|
2025-05-09 16:31:14 +01:00
|
|
|
from flask_caching import Cache
|
|
|
|
import requests.auth
|
|
|
|
import os
|
|
|
|
import base64
|
2025-07-15 15:44:19 +01:00
|
|
|
from typing import Dict, Any, Tuple, Union
|
2025-07-18 08:49:26 +01:00
|
|
|
import sys
|
|
|
|
import redis
|
2025-07-18 09:50:02 +01:00
|
|
|
import json
|
|
|
|
import mysql.connector
|
2025-07-23 09:26:06 +01:00
|
|
|
import re
|
2025-07-23 09:28:17 +01:00
|
|
|
import threading
|
2025-07-15 15:44:19 +01:00
|
|
|
|
|
|
|
from lib.datetime import filter_accounts_next_30_days, filter_accounts_expired
|
|
|
|
from lib.reqs import (get_urls, get_user_accounts, add_user_account,
|
|
|
|
delete_user_account, get_stream_names)
|
2025-07-13 16:00:52 +01:00
|
|
|
from config import DevelopmentConfig, ProductionConfig
|
2025-05-09 16:31:14 +01:00
|
|
|
|
2025-07-13 16:00:52 +01:00
|
|
|
|
2025-05-09 16:31:14 +01:00
|
|
|
os.environ["OMP_NUM_THREADS"] = "1"
|
|
|
|
os.environ["MKL_NUM_THREADS"] = "1"
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
2025-07-13 16:00:52 +01:00
|
|
|
|
|
|
|
if os.environ.get("FLASK_ENV") == "production":
|
|
|
|
app.config.from_object(ProductionConfig)
|
|
|
|
else:
|
|
|
|
app.config.from_object(DevelopmentConfig)
|
2025-07-15 15:44:19 +01:00
|
|
|
|
2025-07-18 08:49:26 +01:00
|
|
|
# Check for Redis availability and configure cache
|
2025-07-18 09:01:13 +01:00
|
|
|
redis_url = app.config["REDIS_URL"]
|
2025-07-18 08:49:26 +01:00
|
|
|
cache_config = {"CACHE_TYPE": "redis", "CACHE_REDIS_URL": redis_url}
|
|
|
|
try:
|
|
|
|
# Use a short timeout to prevent hanging
|
|
|
|
r = redis.from_url(redis_url, socket_connect_timeout=1)
|
|
|
|
r.ping()
|
|
|
|
except redis.exceptions.ConnectionError as e:
|
|
|
|
print(
|
|
|
|
f"WARNING: Redis connection failed: {e}. Falling back to SimpleCache. "
|
|
|
|
"This is not recommended for production with multiple workers.",
|
|
|
|
file=sys.stderr,
|
|
|
|
)
|
|
|
|
cache_config = {"CACHE_TYPE": "SimpleCache"}
|
|
|
|
|
|
|
|
cache = Cache(app, config=cache_config)
|
2025-05-09 16:31:14 +01:00
|
|
|
|
2025-07-18 11:51:01 +01:00
|
|
|
app.config["OCR_ENABLED"] = False
|
2025-05-09 16:31:14 +01:00
|
|
|
|
2025-07-13 16:00:52 +01:00
|
|
|
app.config["SESSION_COOKIE_SECURE"] = not app.config["DEBUG"]
|
2025-07-15 15:44:19 +01:00
|
|
|
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
|
|
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
|
|
|
app.config['PERMANENT_SESSION_LIFETIME'] = 60 * 60 * 24 * 365 # 1 year
|
|
|
|
|
|
|
|
def get_version() -> str:
|
|
|
|
"""Retrieves the application version from the VERSION file.
|
2025-05-09 16:31:14 +01:00
|
|
|
|
2025-07-15 15:44:19 +01:00
|
|
|
Returns:
|
|
|
|
The version string, or 'dev' if the file is not found.
|
|
|
|
"""
|
2025-07-09 10:59:28 +01:00
|
|
|
try:
|
|
|
|
with open('VERSION', 'r') as f:
|
|
|
|
return f.read().strip()
|
|
|
|
except FileNotFoundError:
|
|
|
|
return 'dev'
|
|
|
|
|
|
|
|
@app.context_processor
|
2025-07-15 15:44:19 +01:00
|
|
|
def inject_version() -> Dict[str, str]:
|
|
|
|
"""Injects the version into all templates."""
|
2025-07-17 15:41:16 +01:00
|
|
|
return dict(version=get_version(), config=app.config, session=session)
|
2025-07-09 10:59:28 +01:00
|
|
|
|
2025-07-18 07:37:20 +01:00
|
|
|
def make_cache_key(*args, **kwargs):
|
|
|
|
"""Generate a cache key based on the user's session and request path."""
|
|
|
|
username = session.get('username', 'anonymous')
|
|
|
|
path = request.path
|
|
|
|
return f"view/{username}/{path}"
|
|
|
|
|
2025-05-09 16:31:14 +01:00
|
|
|
@app.before_request
|
2025-07-15 15:44:19 +01:00
|
|
|
def make_session_permanent() -> None:
|
|
|
|
"""Makes the user session permanent."""
|
2025-05-09 16:31:14 +01:00
|
|
|
session.permanent = True
|
|
|
|
|
2025-07-15 10:20:06 +01:00
|
|
|
@app.route('/site.webmanifest')
|
2025-07-15 15:44:19 +01:00
|
|
|
def serve_manifest() -> Response:
|
|
|
|
"""Serves the site manifest file."""
|
|
|
|
return send_from_directory(
|
|
|
|
os.path.join(app.root_path, 'static'),
|
|
|
|
'site.webmanifest',
|
|
|
|
mimetype='application/manifest+json'
|
|
|
|
)
|
2025-05-09 16:31:14 +01:00
|
|
|
|
|
|
|
@app.route("/favicon.ico")
|
2025-07-15 15:44:19 +01:00
|
|
|
def favicon() -> Response:
|
|
|
|
"""Serves the favicon."""
|
2025-05-09 16:31:14 +01:00
|
|
|
return send_from_directory(
|
|
|
|
os.path.join(app.root_path, "static"),
|
|
|
|
"favicon.ico",
|
|
|
|
mimetype="image/vnd.microsoft.icon",
|
|
|
|
)
|
|
|
|
|
|
|
|
@app.route("/")
|
2025-07-15 15:44:19 +01:00
|
|
|
def index() -> Union[Response, str]:
|
|
|
|
"""Renders the index page or redirects to home if logged in."""
|
2025-05-09 16:31:14 +01:00
|
|
|
if session.get("logged_in"):
|
|
|
|
return redirect(url_for("home"))
|
|
|
|
return render_template("index.html")
|
|
|
|
|
2025-07-17 16:58:24 +01:00
|
|
|
@app.route('/vapid-public-key', methods=['GET'])
|
|
|
|
def proxy_vapid_public_key():
|
|
|
|
"""Proxies the request for the VAPID public key to the backend."""
|
2025-07-19 11:05:09 +01:00
|
|
|
backend_url = f"{app.config['BACKEND_URL']}/vapid-public-key"
|
2025-07-17 16:58:24 +01:00
|
|
|
try:
|
|
|
|
response = requests.get(backend_url)
|
|
|
|
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
return jsonify({"error": str(e)}), 502
|
2025-07-17 16:22:56 +01:00
|
|
|
|
|
|
|
@app.route('/save-subscription', methods=['POST'])
|
2025-07-17 16:58:24 +01:00
|
|
|
def proxy_save_subscription():
|
|
|
|
"""Proxies the request to save a push subscription to the backend."""
|
2025-07-17 16:22:56 +01:00
|
|
|
if not session.get("logged_in"):
|
|
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
|
|
|
2025-07-19 11:05:09 +01:00
|
|
|
backend_url = f"{app.config['BACKEND_URL']}/save-subscription"
|
2025-07-17 16:58:24 +01:00
|
|
|
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
|
|
|
username, password = credentials.split(":", 1)
|
2025-07-17 16:22:56 +01:00
|
|
|
|
2025-07-17 16:58:24 +01:00
|
|
|
try:
|
|
|
|
response = requests.post(
|
|
|
|
backend_url,
|
|
|
|
auth=requests.auth.HTTPBasicAuth(username, password),
|
|
|
|
json=request.get_json()
|
|
|
|
)
|
|
|
|
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
2025-07-17 19:05:59 +01:00
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
return jsonify({"error": str(e)}), 502
|
|
|
|
|
|
|
|
@app.route('/send-test-notification', methods=['POST'])
|
2025-07-18 09:50:02 +01:00
|
|
|
def send_test_notification():
|
2025-07-18 16:18:18 +01:00
|
|
|
"""Proxies the request to send a test notification to the backend."""
|
2025-07-17 19:05:59 +01:00
|
|
|
if not session.get("logged_in"):
|
|
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
|
|
|
2025-07-19 11:05:09 +01:00
|
|
|
backend_url = f"{app.config['BACKEND_URL']}/send-test-notification"
|
2025-07-18 16:18:18 +01:00
|
|
|
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
|
|
|
username, password = credentials.split(":", 1)
|
2025-07-18 09:50:02 +01:00
|
|
|
|
2025-07-18 16:18:18 +01:00
|
|
|
try:
|
|
|
|
response = requests.post(
|
|
|
|
backend_url,
|
|
|
|
auth=requests.auth.HTTPBasicAuth(username, password),
|
2025-07-18 16:31:39 +01:00
|
|
|
json={}
|
2025-07-18 16:18:18 +01:00
|
|
|
)
|
|
|
|
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
return jsonify({"error": str(e)}), 502
|
2025-07-17 16:22:56 +01:00
|
|
|
|
2025-05-09 16:31:14 +01:00
|
|
|
@app.route("/home")
|
2025-07-18 07:37:20 +01:00
|
|
|
@cache.cached(timeout=60, key_prefix=make_cache_key)
|
2025-07-15 15:44:19 +01:00
|
|
|
def home() -> str:
|
|
|
|
"""Renders the home page with account statistics."""
|
2025-05-09 16:31:14 +01:00
|
|
|
if session.get("logged_in"):
|
2025-07-19 11:05:09 +01:00
|
|
|
base_url = app.config["BACKEND_URL"]
|
2025-05-09 16:31:14 +01:00
|
|
|
all_accounts = get_user_accounts(base_url, session["auth_credentials"])
|
|
|
|
return render_template(
|
|
|
|
"home.html",
|
|
|
|
username=session["username"],
|
2025-07-15 15:44:19 +01:00
|
|
|
accounts=len(all_accounts),
|
|
|
|
current_month_accounts=filter_accounts_next_30_days(all_accounts),
|
|
|
|
expired_accounts=filter_accounts_expired(all_accounts),
|
2025-05-09 16:31:14 +01:00
|
|
|
)
|
|
|
|
return render_template("index.html")
|
|
|
|
|
|
|
|
@app.route("/login", methods=["POST"])
|
2025-07-15 15:44:19 +01:00
|
|
|
def login() -> Union[Response, str]:
|
|
|
|
"""Handles user login."""
|
2025-05-09 16:31:14 +01:00
|
|
|
username = request.form["username"]
|
|
|
|
password = request.form["password"]
|
|
|
|
credentials = f"{username}:{password}"
|
|
|
|
encoded_credentials = base64.b64encode(credentials.encode()).decode()
|
2025-07-19 11:05:09 +01:00
|
|
|
base_url = app.config["BACKEND_URL"]
|
2025-07-15 15:44:19 +01:00
|
|
|
login_url = f"{base_url}/Login"
|
2025-05-09 16:31:14 +01:00
|
|
|
|
2025-07-15 15:44:19 +01:00
|
|
|
try:
|
|
|
|
response = requests.get(
|
|
|
|
login_url, auth=requests.auth.HTTPBasicAuth(username, password)
|
|
|
|
)
|
|
|
|
response.raise_for_status()
|
2025-07-23 09:26:06 +01:00
|
|
|
response_data = response.json()
|
|
|
|
if response_data.get("auth") == "Success":
|
2025-07-15 15:44:19 +01:00
|
|
|
session["logged_in"] = True
|
2025-07-23 09:26:06 +01:00
|
|
|
session["username"] = response_data.get("username", username)
|
|
|
|
session["user_id"] = response_data.get("user_id")
|
2025-07-15 15:44:19 +01:00
|
|
|
session["auth_credentials"] = encoded_credentials
|
2025-07-17 10:05:14 +01:00
|
|
|
next_url = request.args.get("next")
|
|
|
|
if next_url:
|
|
|
|
return redirect(next_url)
|
2025-07-17 18:44:09 +01:00
|
|
|
return redirect(url_for("home", loggedin=True))
|
2025-07-15 15:44:19 +01:00
|
|
|
except requests.exceptions.RequestException:
|
|
|
|
pass # Fall through to error
|
|
|
|
|
|
|
|
error = "Invalid username or password. Please try again."
|
|
|
|
return render_template("index.html", error=error)
|
2025-05-09 16:31:14 +01:00
|
|
|
|
|
|
|
@app.route("/urls", methods=["GET"])
|
2025-07-18 07:37:20 +01:00
|
|
|
@cache.cached(timeout=300, key_prefix=make_cache_key)
|
2025-07-15 15:44:19 +01:00
|
|
|
def urls() -> Union[Response, str]:
|
|
|
|
"""Renders the URLs page."""
|
2025-05-09 16:31:14 +01:00
|
|
|
if not session.get("logged_in"):
|
|
|
|
return redirect(url_for("home"))
|
2025-07-19 11:05:09 +01:00
|
|
|
base_url = app.config["BACKEND_URL"]
|
2025-05-09 16:31:14 +01:00
|
|
|
return render_template(
|
|
|
|
"urls.html", urls=get_urls(base_url, session["auth_credentials"])
|
|
|
|
)
|
|
|
|
|
|
|
|
@app.route("/accounts", methods=["GET"])
|
2025-07-18 07:37:20 +01:00
|
|
|
@cache.cached(timeout=60, key_prefix=make_cache_key)
|
2025-07-15 15:44:19 +01:00
|
|
|
def user_accounts() -> Union[Response, str]:
|
|
|
|
"""Renders the user accounts page."""
|
2025-05-09 16:31:14 +01:00
|
|
|
if not session.get("logged_in"):
|
|
|
|
return redirect(url_for("home"))
|
2025-07-19 11:05:09 +01:00
|
|
|
base_url = app.config["BACKEND_URL"]
|
2025-07-14 20:09:17 +01:00
|
|
|
user_accounts_data = get_user_accounts(base_url, session["auth_credentials"])
|
2025-05-09 16:31:14 +01:00
|
|
|
return render_template(
|
|
|
|
"user_accounts.html",
|
|
|
|
username=session["username"],
|
2025-07-14 20:09:17 +01:00
|
|
|
user_accounts=user_accounts_data,
|
2025-05-09 16:31:14 +01:00
|
|
|
auth=session["auth_credentials"],
|
|
|
|
)
|
|
|
|
|
2025-07-17 07:43:00 +01:00
|
|
|
@app.route("/share", methods=["GET"])
|
|
|
|
def share() -> Response:
|
|
|
|
"""Handles shared text from PWA."""
|
2025-07-17 10:05:14 +01:00
|
|
|
if not session.get("logged_in"):
|
|
|
|
return redirect(url_for("index", next=request.url))
|
2025-07-17 07:43:00 +01:00
|
|
|
shared_text = request.args.get("text")
|
|
|
|
return redirect(url_for("add_account", shared_text=shared_text))
|
|
|
|
|
2025-05-09 16:31:14 +01:00
|
|
|
@app.route("/accounts/add", methods=["GET", "POST"])
|
2025-07-15 15:44:19 +01:00
|
|
|
def add_account() -> Union[Response, str]:
|
|
|
|
"""Handles adding a new user account."""
|
2025-07-17 10:05:14 +01:00
|
|
|
if not session.get("logged_in"):
|
|
|
|
return redirect(url_for("index", next=request.url))
|
2025-07-19 11:05:09 +01:00
|
|
|
base_url = app.config["BACKEND_URL"]
|
2025-07-15 08:02:51 +01:00
|
|
|
shared_text = request.args.get('shared_text')
|
|
|
|
|
2025-05-09 16:31:14 +01:00
|
|
|
if request.method == "POST":
|
|
|
|
username = request.form["username"]
|
|
|
|
password = request.form["password"]
|
|
|
|
stream = request.form["stream"]
|
|
|
|
if add_user_account(
|
|
|
|
base_url, session["auth_credentials"], username, password, stream
|
|
|
|
):
|
2025-07-18 07:37:20 +01:00
|
|
|
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
|
2025-07-23 09:28:17 +01:00
|
|
|
# Run the NPM config update in a background thread
|
|
|
|
thread = threading.Thread(target=_update_npm_config_in_background)
|
|
|
|
thread.start()
|
2025-05-09 16:31:14 +01:00
|
|
|
return redirect(url_for("user_accounts"))
|
|
|
|
|
2025-07-15 15:44:19 +01:00
|
|
|
return render_template(
|
|
|
|
"add_account.html",
|
|
|
|
text_input_enabled=app.config.get("TEXT_INPUT_ENABLED"),
|
|
|
|
shared_text=shared_text
|
|
|
|
)
|
2025-05-09 16:31:14 +01:00
|
|
|
|
|
|
|
@app.route("/accounts/delete", methods=["POST"])
|
2025-07-15 15:44:19 +01:00
|
|
|
def delete_account() -> Response:
|
|
|
|
"""Handles deleting a user account."""
|
2025-05-09 16:31:14 +01:00
|
|
|
stream = request.form.get("stream")
|
|
|
|
username = request.form.get("username")
|
2025-07-19 11:05:09 +01:00
|
|
|
base_url = app.config["BACKEND_URL"]
|
2025-07-15 15:44:19 +01:00
|
|
|
delete_user_account(base_url, session["auth_credentials"], stream, username)
|
2025-07-18 07:37:20 +01:00
|
|
|
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
|
2025-05-09 16:31:14 +01:00
|
|
|
return redirect(url_for("user_accounts"))
|
|
|
|
|
2025-07-15 15:13:16 +01:00
|
|
|
@app.route("/validateAccount", methods=["POST"])
|
2025-07-15 15:44:19 +01:00
|
|
|
def validate_account() -> Tuple[Response, int]:
|
|
|
|
"""Forwards account validation requests to the backend."""
|
2025-07-19 11:05:09 +01:00
|
|
|
base_url = app.config["BACKEND_URL"]
|
2025-07-15 15:13:16 +01:00
|
|
|
validate_url = f"{base_url}/validateAccount"
|
|
|
|
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
|
|
|
username, password = credentials.split(":", 1)
|
|
|
|
|
2025-07-15 15:44:19 +01:00
|
|
|
try:
|
|
|
|
response = requests.post(
|
|
|
|
validate_url,
|
|
|
|
auth=requests.auth.HTTPBasicAuth(username, password),
|
|
|
|
json=request.get_json()
|
|
|
|
)
|
|
|
|
response.raise_for_status()
|
2025-07-16 09:09:05 +01:00
|
|
|
response_data = response.json()
|
|
|
|
if response_data.get("message") == "Account is valid and updated":
|
2025-07-18 07:37:20 +01:00
|
|
|
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
|
2025-07-23 09:28:17 +01:00
|
|
|
# Run the NPM config update in a background thread
|
|
|
|
thread = threading.Thread(target=_update_npm_config_in_background)
|
|
|
|
thread.start()
|
2025-07-16 09:09:05 +01:00
|
|
|
return jsonify(response_data), response.status_code
|
2025-07-15 15:44:19 +01:00
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
return jsonify({"error": str(e)}), 500
|
2025-07-15 15:13:16 +01:00
|
|
|
|
2025-07-05 10:57:19 +01:00
|
|
|
@app.route("/get_stream_names", methods=["GET"])
|
2025-07-15 15:44:19 +01:00
|
|
|
def stream_names() -> Union[Response, str]:
|
|
|
|
"""Fetches and returns stream names as JSON."""
|
2025-07-05 10:57:19 +01:00
|
|
|
if not session.get("logged_in"):
|
|
|
|
return redirect(url_for("home"))
|
2025-07-19 11:05:09 +01:00
|
|
|
base_url = app.config["BACKEND_URL"]
|
2025-07-15 15:44:19 +01:00
|
|
|
return jsonify(get_stream_names(base_url, session["auth_credentials"]))
|
2025-07-05 10:57:19 +01:00
|
|
|
|
2025-05-09 16:31:14 +01:00
|
|
|
|
2025-07-23 09:26:06 +01:00
|
|
|
@app.route('/config')
|
2025-07-19 08:56:13 +01:00
|
|
|
def config():
|
|
|
|
"""Handles access to the configuration page."""
|
2025-07-23 09:26:06 +01:00
|
|
|
if session.get('user_id') and int(session.get('user_id')) == 1:
|
|
|
|
return redirect(url_for('config_dashboard'))
|
|
|
|
return redirect(url_for('home'))
|
2025-07-19 08:56:13 +01:00
|
|
|
|
|
|
|
@app.route('/config/dashboard')
|
|
|
|
def config_dashboard():
|
|
|
|
"""Renders the configuration dashboard."""
|
2025-07-23 09:26:06 +01:00
|
|
|
if not session.get('user_id') or int(session.get('user_id')) != 1:
|
|
|
|
return redirect(url_for('home'))
|
2025-07-19 10:08:37 +01:00
|
|
|
return render_template('config_dashboard.html')
|
2025-07-19 08:56:13 +01:00
|
|
|
|
2025-07-19 09:05:28 +01:00
|
|
|
@app.route('/check-expiring-accounts', methods=['POST'])
|
|
|
|
def check_expiring_accounts():
|
|
|
|
"""Proxies the request to check for expiring accounts to the backend."""
|
2025-07-23 09:26:06 +01:00
|
|
|
if not session.get('user_id') or int(session.get('user_id')) != 1:
|
2025-07-19 09:05:28 +01:00
|
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
|
|
|
2025-07-19 11:05:09 +01:00
|
|
|
backend_url = f"{app.config['BACKEND_URL']}/check-expiry"
|
2025-07-19 09:05:28 +01:00
|
|
|
try:
|
|
|
|
response = requests.post(backend_url)
|
|
|
|
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
return jsonify({"error": str(e)}), 502
|
|
|
|
|
2025-07-19 08:56:13 +01:00
|
|
|
|
2025-07-19 11:05:09 +01:00
|
|
|
@app.route('/dns', methods=['GET', 'POST', 'DELETE'])
|
|
|
|
def proxy_dns():
|
|
|
|
"""Proxies DNS management requests to the backend."""
|
2025-07-23 09:26:06 +01:00
|
|
|
if not session.get('user_id') or int(session.get('user_id')) != 1:
|
2025-07-19 11:05:09 +01:00
|
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
|
|
|
|
|
|
backend_url = f"{app.config['BACKEND_URL']}/dns"
|
|
|
|
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
2025-07-19 11:31:00 +01:00
|
|
|
username, password = credentials.split(":", 1)
|
|
|
|
auth = requests.auth.HTTPBasicAuth(username, password)
|
|
|
|
|
|
|
|
try:
|
|
|
|
if request.method == 'GET':
|
|
|
|
response = requests.get(backend_url, auth=auth)
|
|
|
|
elif request.method == 'POST':
|
|
|
|
response = requests.post(backend_url, auth=auth, json=request.get_json())
|
|
|
|
if response.ok:
|
|
|
|
cache.clear()
|
|
|
|
elif request.method == 'DELETE':
|
|
|
|
response = requests.delete(backend_url, auth=auth, json=request.get_json())
|
|
|
|
if response.ok:
|
|
|
|
cache.clear()
|
|
|
|
|
|
|
|
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
return jsonify({"error": str(e)}), 502
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/extra_urls', methods=['GET', 'POST', 'DELETE'])
|
|
|
|
def proxy_extra_urls():
|
|
|
|
"""Proxies extra URL management requests to the backend."""
|
2025-07-23 09:26:06 +01:00
|
|
|
if not session.get('user_id') or int(session.get('user_id')) != 1:
|
2025-07-19 11:31:00 +01:00
|
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
|
|
|
|
|
|
backend_url = f"{app.config['BACKEND_URL']}/extra_urls"
|
|
|
|
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
2025-07-19 11:05:09 +01:00
|
|
|
username, password = credentials.split(":", 1)
|
|
|
|
auth = requests.auth.HTTPBasicAuth(username, password)
|
|
|
|
|
|
|
|
try:
|
|
|
|
if request.method == 'GET':
|
|
|
|
response = requests.get(backend_url, auth=auth)
|
|
|
|
elif request.method == 'POST':
|
|
|
|
response = requests.post(backend_url, auth=auth, json=request.get_json())
|
|
|
|
if response.ok:
|
|
|
|
cache.clear()
|
|
|
|
elif request.method == 'DELETE':
|
|
|
|
response = requests.delete(backend_url, auth=auth, json=request.get_json())
|
|
|
|
if response.ok:
|
|
|
|
cache.clear()
|
|
|
|
|
|
|
|
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
return jsonify({"error": str(e)}), 502
|
|
|
|
|
|
|
|
|
2025-07-23 09:26:06 +01:00
|
|
|
class NginxProxyManager:
|
|
|
|
def __init__(self, host, email, password):
|
|
|
|
self.host = host
|
|
|
|
self.email = email
|
|
|
|
self.password = password
|
|
|
|
self.token = None
|
|
|
|
|
|
|
|
def login(self):
|
|
|
|
url = f"{self.host}/api/tokens"
|
|
|
|
payload = {
|
|
|
|
"identity": self.email,
|
|
|
|
"secret": self.password
|
|
|
|
}
|
|
|
|
headers = {
|
|
|
|
"Content-Type": "application/json"
|
|
|
|
}
|
|
|
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
|
|
|
if response.status_code == 200:
|
|
|
|
self.token = response.json()["token"]
|
|
|
|
print("Login successful.")
|
|
|
|
else:
|
|
|
|
print(f"Failed to login: {response.text}")
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
def get_proxy_host(self, host_id):
|
|
|
|
if not self.token:
|
|
|
|
self.login()
|
|
|
|
|
|
|
|
url = f"{self.host}/api/nginx/proxy-hosts/{host_id}"
|
|
|
|
headers = {
|
|
|
|
"Authorization": f"Bearer {self.token}"
|
|
|
|
}
|
|
|
|
response = requests.get(url, headers=headers)
|
|
|
|
if response.status_code == 200:
|
|
|
|
return response.json()
|
|
|
|
else:
|
|
|
|
print(f"Failed to get proxy host {host_id}: {response.text}")
|
|
|
|
return None
|
|
|
|
|
|
|
|
def update_proxy_host_config(self, host_id, config):
|
|
|
|
if not self.token:
|
|
|
|
self.login()
|
|
|
|
|
|
|
|
url = f"{self.host}/api/nginx/proxy-hosts/{host_id}"
|
|
|
|
|
|
|
|
original_host_data = self.get_proxy_host(host_id)
|
|
|
|
if not original_host_data:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Construct a new payload with only the allowed fields for an update
|
|
|
|
update_payload = {
|
|
|
|
"domain_names": original_host_data.get("domain_names", []),
|
|
|
|
"forward_scheme": original_host_data.get("forward_scheme", "http"),
|
|
|
|
"forward_host": original_host_data.get("forward_host"),
|
|
|
|
"forward_port": original_host_data.get("forward_port"),
|
|
|
|
"access_list_id": original_host_data.get("access_list_id", 0),
|
|
|
|
"certificate_id": original_host_data.get("certificate_id", 0),
|
|
|
|
"ssl_forced": original_host_data.get("ssl_forced", False),
|
|
|
|
"hsts_enabled": original_host_data.get("hsts_enabled", False),
|
|
|
|
"hsts_subdomains": original_host_data.get("hsts_subdomains", False),
|
|
|
|
"http2_support": original_host_data.get("http2_support", False),
|
|
|
|
"block_exploits": original_host_data.get("block_exploits", False),
|
|
|
|
"caching_enabled": original_host_data.get("caching_enabled", False),
|
|
|
|
"allow_websocket_upgrade": original_host_data.get("allow_websocket_upgrade", False),
|
|
|
|
"advanced_config": config, # The updated advanced config
|
|
|
|
"meta": original_host_data.get("meta", {}),
|
|
|
|
"locations": original_host_data.get("locations", []),
|
|
|
|
}
|
|
|
|
|
|
|
|
headers = {
|
|
|
|
"Authorization": f"Bearer {self.token}",
|
|
|
|
"Content-Type": "application/json"
|
|
|
|
}
|
|
|
|
|
|
|
|
response = requests.put(url, headers=headers, data=json.dumps(update_payload))
|
|
|
|
if response.status_code == 200:
|
|
|
|
print(f"Successfully updated proxy host {host_id}")
|
|
|
|
else:
|
|
|
|
print(f"Failed to update proxy host {host_id}: {response.text}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_config_with_streams(config, streams):
|
|
|
|
# Get all stream names from the database
|
|
|
|
db_stream_names = {stream['streamName'] for stream in streams}
|
|
|
|
|
|
|
|
# Find all location blocks in the config
|
|
|
|
location_blocks = re.findall(r'location ~ \^/(\w+)\(\.\*\)\$ \{[^}]+\}', config)
|
|
|
|
|
|
|
|
# Remove location blocks that are not in the database
|
|
|
|
for stream_name in location_blocks:
|
|
|
|
if stream_name not in db_stream_names:
|
|
|
|
print(f"Removing location block for stream: {stream_name}")
|
|
|
|
pattern = re.compile(f'location ~ \\^/{re.escape(stream_name)}\\(\\.\\*\\)\\$ {{[^}}]+}}\\s*', re.DOTALL)
|
|
|
|
config = pattern.sub('', config)
|
|
|
|
|
|
|
|
# Update existing stream URLs
|
|
|
|
for stream in streams:
|
|
|
|
stream_name = stream['streamName']
|
|
|
|
stream_url = stream['streamURL']
|
|
|
|
if stream_url: # Ensure there is a URL to update to
|
|
|
|
# Use a more specific regex to avoid replacing parts of other URLs
|
|
|
|
pattern = re.compile(f'(location ~ \\^/{re.escape(stream_name)}\\(\\.\\*\\)\\$ {{\\s*return 302 )([^;]+)(;\\s*}})')
|
|
|
|
config = pattern.sub(f'\\1{stream_url}/$1$is_args$args\\3', config)
|
|
|
|
|
|
|
|
return config
|
|
|
|
|
2025-07-23 09:28:17 +01:00
|
|
|
def _update_npm_config():
|
|
|
|
"""Helper function to update the NPM config."""
|
2025-07-23 09:26:06 +01:00
|
|
|
if not session.get('user_id') or int(session.get('user_id')) != 1:
|
2025-07-23 09:28:17 +01:00
|
|
|
print("Unauthorized attempt to update NPM config.")
|
|
|
|
return
|
2025-07-23 09:26:06 +01:00
|
|
|
|
|
|
|
npm = NginxProxyManager(app.config['NPM_HOST'], app.config['NPM_EMAIL'], app.config['NPM_PASSWORD'])
|
|
|
|
npm.login()
|
|
|
|
host = npm.get_proxy_host(9)
|
|
|
|
if host:
|
|
|
|
current_config = host.get('advanced_config', '')
|
|
|
|
|
|
|
|
backend_url = f"{app.config['BACKEND_URL']}/get_all_stream_urls"
|
|
|
|
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
|
|
|
username, password = credentials.split(":", 1)
|
|
|
|
auth = requests.auth.HTTPBasicAuth(username, password)
|
|
|
|
|
|
|
|
try:
|
|
|
|
response = requests.get(backend_url, auth=auth)
|
|
|
|
response.raise_for_status()
|
|
|
|
streams = response.json()
|
|
|
|
except requests.exceptions.RequestException as e:
|
2025-07-23 09:28:17 +01:00
|
|
|
print(f"Failed to fetch streams from backend: {e}")
|
|
|
|
return
|
2025-07-23 09:26:06 +01:00
|
|
|
|
|
|
|
if streams:
|
|
|
|
new_config = update_config_with_streams(current_config, streams)
|
|
|
|
npm.update_proxy_host_config(9, new_config)
|
2025-07-23 09:28:17 +01:00
|
|
|
print("NPM config updated successfully.")
|
|
|
|
else:
|
|
|
|
print("Failed to update NPM config.")
|
|
|
|
|
|
|
|
def _update_npm_config_in_background():
|
|
|
|
with app.app_context():
|
|
|
|
_update_npm_config()
|
|
|
|
|
|
|
|
@app.route('/update_host_9_config', methods=['POST'])
|
|
|
|
def update_host_9_config():
|
|
|
|
if not session.get('user_id') or int(session.get('user_id')) != 1:
|
|
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
|
|
|
|
|
|
thread = threading.Thread(target=_update_npm_config_in_background)
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
return jsonify({'message': 'NPM config update started in the background.'}), 202
|
2025-07-23 09:26:06 +01:00
|
|
|
|
|
|
|
|
2025-05-09 16:31:14 +01:00
|
|
|
if __name__ == "__main__":
|
2025-07-15 15:44:19 +01:00
|
|
|
app.run(
|
|
|
|
debug=app.config["DEBUG"],
|
|
|
|
host=app.config["HOST"],
|
|
|
|
port=app.config["PORT"]
|
|
|
|
)
|