From 0f1080b196ecb3a369b3f704d10222e98e66104b Mon Sep 17 00:00:00 2001 From: Karl Date: Wed, 23 Jul 2025 09:26:11 +0100 Subject: [PATCH] rework config login and add update NPM function --- .vscode/launch.json | 2 +- generate_vapid_keys.py | 21 +++++ host_9_config.txt | 72 +++++++++++++++++ ktvmanager/lib/auth.py | 4 +- ktvmanager/lib/database.py | 11 +++ npm_config_modifier.py | 157 +++++++++++++++++++++++++++++++++++++ routes/api.py | 16 ++++ 7 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 generate_vapid_keys.py create mode 100644 host_9_config.txt create mode 100644 npm_config_modifier.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 8623304..040e51d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,7 @@ "FLASK_APP": "ktvmanager.main:create_app", "FLASK_ENV": "development", "PYTHONPATH": "${workspaceFolder}", - "FLASK_RUN_PORT": "5001" + "FLASK_RUN_PORT": "5002" }, "args": [ "run", diff --git a/generate_vapid_keys.py b/generate_vapid_keys.py new file mode 100644 index 0000000..589b646 --- /dev/null +++ b/generate_vapid_keys.py @@ -0,0 +1,21 @@ +from py_vapid import Vapid +import os + +vapid = Vapid() +vapid.generate_keys() + +from cryptography.hazmat.primitives import serialization + +private_key = vapid.private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() +).decode('utf-8') + +public_key = vapid.public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo +).decode('utf-8') + +print(f"VAPID_PRIVATE_KEY='{private_key}'") +print(f"VAPID_PUBLIC_KEY='{public_key}'") \ No newline at end of file diff --git a/host_9_config.txt b/host_9_config.txt new file mode 100644 index 0000000..1b3e54d --- /dev/null +++ b/host_9_config.txt @@ -0,0 +1,72 @@ +location ~ ^/Mongoose(.*)$ { + return 302 http://m3u.sstv.one:81/$1$is_args$args; +} +location ~ ^/Blazin(.*)$ { + return 302 http://blazin.dns-cloud.net:8080/$1$is_args$args; +} +location ~ ^/Insanity(.*)$ { + return 302 https://biglicks.win:443/$1$is_args$args; +} +location ~ ^/Badger(.*)$ { + return 302 http://hurricanetv.kiev.ua:80/$1$is_args$args; +} +location ~ ^/Gunslinger(.*)$ { + return 302 http://jabawalkies.club:8080/$1$is_args$args; +} +location ~ ^/KDB(.*)$ { + return 302 http://finger-ya-bum-hole.site/$1$is_args$args; +} +location ~ ^/Graphite(.*)$ { + return 302 http://sarahgraphite.liveme.vip:80/$1$is_args$args; +} +location ~ ^/old-Premium(.*)$ { + return 302 https://kwikfitfitter.life:443/$1$is_args$args; +} +location ~ ^/Gold(.*)$ { + return 302 http://server1.elitehosting.gq:8090/$1$is_args$args; +} +location ~ ^/Bravado(.*)$ { + return 302 http://le.thund.re/$1$is_args$args; +} +location ~ ^/Titan(.*)$ { + return 302 http://maximumorg.xyz:80/$1$is_args$args; +} +location ~ ^/Wolfie(.*)$ { + return 302 http://deviltv.fun:8080/$1$is_args$args; +} +location ~ ^/DiamondBack(.*)$ { + return 302 http://pro-media.live:2052/$1$is_args$args; +} +location ~ ^/Halo(.*)$ { + return 302 http://i-like-turtles.org:8080/$1$is_args$args; +} +location ~ ^/Nitro(.*)$ { + return 302 http://mr-beans-streams.xyz$1$is_args$args; +} +location ~ ^/Insanity(.*)$ { + return 302 https://biglicks.win:443/$1$is_args$args; +} +location ~ ^/Bonsai(.*)$ { + return 302 http://crazyservertimes.pro/$1$is_args$args; +} +location ~ ^/New-Prem(.*)$ { + return 302 http://hello.exodus-2.xyz:8080/$1$is_args$args; +} +location ~ ^/Crystal(.*)$ { + return 302 https://line.ottcst.com/$1$is_args$args; +} +location ~ ^/VIP(.*)$ { + return 302 https://1visions.co.uk:443/$1$is_args$args; +} +location ~ ^/WILD(.*)$ { + return 302 http://wildversion.com:8080/$1$is_args$args; +} +location ~ ^/STEST(.*)$ { + return 302 http://notwhatyourlookingfor.ru/$1$is_args$args; +} +location ~ ^/SPARE(.*)$ { + return 302 http://moontv.co.uk/$1$is_args$args; +} +location ~ ^/QUARTZ(.*)$ { + return 302 http://anyholeisagoal.ru/$1$is_args$args; +} \ No newline at end of file diff --git a/ktvmanager/lib/auth.py b/ktvmanager/lib/auth.py index 89166b9..c693ed6 100644 --- a/ktvmanager/lib/auth.py +++ b/ktvmanager/lib/auth.py @@ -1,6 +1,7 @@ from functools import wraps from flask import request, jsonify, Blueprint, Response from typing import Callable, Any, Tuple, Dict +from .database import get_user_id_from_username auth_blueprint = Blueprint("auth", __name__) @@ -57,4 +58,5 @@ def check_login(username: str, password: str) -> Response: Returns: A Flask JSON response indicating success. """ - return jsonify({"auth": "Success"}) \ No newline at end of file + user_id = get_user_id_from_username(username) + return jsonify({"auth": "Success", "user_id": user_id, "username": username}) \ No newline at end of file diff --git a/ktvmanager/lib/database.py b/ktvmanager/lib/database.py index 4ff2c26..1e247d7 100644 --- a/ktvmanager/lib/database.py +++ b/ktvmanager/lib/database.py @@ -114,6 +114,17 @@ def get_stream_names() -> Response: return jsonify(stream_names) +def get_all_stream_urls() -> Response: + """Retrieves all stream names and URLs from the database. + + Returns: + A Flask JSON response containing a list of stream names and URLs. + """ + query = "SELECT DISTINCT SUBSTRING_INDEX(stream, ' ', 1) AS streamName, streamURL FROM userAccounts" + results = _execute_query(query) + return jsonify(results) + + def single_check() -> Response | Tuple[Response, int]: """ Performs a check on a single account provided in the request JSON. diff --git a/npm_config_modifier.py b/npm_config_modifier.py new file mode 100644 index 0000000..0a6980a --- /dev/null +++ b/npm_config_modifier.py @@ -0,0 +1,157 @@ +import requests +import json +import argparse +import mysql.connector +import re +import os +from dotenv import load_dotenv + +load_dotenv() + +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_hosts(self): + if not self.token: + self.login() + + url = f"{self.host}/api/nginx/proxy-hosts" + 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 hosts: {response.text}") + return [] + + 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}" + payload = { + "advanced_config": config + } + headers = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json" + } + response = requests.put(url, headers=headers, data=json.dumps(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 get_streams_from_db(db_host, db_user, db_pass, db_name, db_port): + try: + conn = mysql.connector.connect( + host=db_host, + user=db_user, + password=db_pass, + database=db_name, + port=db_port + ) + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT streamName, streamURL FROM streams") + streams = cursor.fetchall() + cursor.close() + conn.close() + return streams + except mysql.connector.Error as err: + print(f"Error connecting to database: {err}") + return [] + +def update_config_with_streams(config, streams): + for stream in streams: + stream_name = stream['streamName'] + stream_url = stream['streamURL'] + # 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 + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Modify Nginx Proxy Manager custom configuration.") + parser.add_argument("--list-hosts", action="store_true", help="List all proxy hosts") + parser.add_argument("--host-id", type=int, help="The ID of the proxy host to modify") + parser.add_argument("--config-file", type=str, help="Path to the file containing the new advanced configuration") + parser.add_argument("--download-config", type=str, help="Path to save the current advanced configuration") + parser.add_argument("--update-from-db", action="store_true", help="Update the configuration from the database") + + args = parser.parse_args() + + npm_host = os.getenv("NPM_HOST") + npm_email = os.getenv("NPM_EMAIL") + npm_password = os.getenv("NPM_PASSWORD") + db_host = os.getenv("DBHOST") + db_user = os.getenv("DBUSER") + db_pass = os.getenv("DBPASS") + db_name = os.getenv("DATABASE") + db_port = os.getenv("DBPORT") + + npm = NginxProxyManager(npm_host, npm_email, npm_password) + npm.login() + + if args.list_hosts: + hosts = npm.get_proxy_hosts() + for host in hosts: + print(f"ID: {host['id']}, Domains: {', '.join(host['domain_names'])}") + + if args.host_id and args.download_config: + host = npm.get_proxy_host(args.host_id) + if host: + with open(args.download_config, 'w') as f: + f.write(host.get('advanced_config', '')) + print(f"Configuration for host {args.host_id} downloaded to {args.download_config}") + + if args.host_id and args.config_file: + with open(args.config_file, 'r') as f: + config = f.read() + npm.update_proxy_host_config(args.host_id, config) + + if args.host_id and args.update_from_db: + host = npm.get_proxy_host(args.host_id) + if host: + current_config = host.get('advanced_config', '') + streams = get_streams_from_db(db_host, db_user, db_pass, db_name, db_port) + if streams: + new_config = update_config_with_streams(current_config, streams) + npm.update_proxy_host_config(args.host_id, new_config) \ No newline at end of file diff --git a/routes/api.py b/routes/api.py index bec76a4..ee31abe 100644 --- a/routes/api.py +++ b/routes/api.py @@ -8,6 +8,7 @@ from ktvmanager.lib.database import ( get_user_id_from_username, save_push_subscription, get_push_subscriptions, + get_all_stream_urls, ) from ktvmanager.lib.get_urls import get_latest_urls_from_dns from ktvmanager.lib.auth import requires_basic_auth, check_login @@ -71,6 +72,21 @@ def get_user_accounts_streams_route(username: str, password: str) -> Response: return jsonify(get_latest_urls_from_dns()) +@api_blueprint.route("/get_all_stream_urls") +@requires_basic_auth +def get_all_stream_urls_route(username: str, password: str) -> Response: + """Retrieves all stream names and URLs. + + Args: + username: The username of the user. + password: The password of the user (used for authentication). + + Returns: + A Flask JSON response containing the list of stream names and URLs. + """ + return get_all_stream_urls() + + @api_blueprint.route("/singleCheck", methods=["POST"]) @requires_basic_auth def single_check_route(username: str, password: str) -> Response: