Compare commits

...

5 Commits
1.4.8 ... main

Author SHA1 Message Date
8b9c100e87 Bump version: 1.4.9 → 1.4.10
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 1m40s
2025-07-23 09:28:24 +01:00
6462fc6009 execute on url update 2025-07-23 09:28:17 +01:00
dbf0161133 rework config login and add update NPM function 2025-07-23 09:26:06 +01:00
0d56235863 Bump version: 1.4.8 → 1.4.9
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 1m28s
2025-07-19 23:19:19 +01:00
5c80751484 cleanup 2025-07-19 23:19:16 +01:00
7 changed files with 355 additions and 67 deletions

View File

@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "1.4.8"
current_version = "1.4.10"
commit = true
tag = true
tag_name = "{new_version}"

View File

@ -1 +1 @@
1.4.8
1.4.10

203
app.py
View File

@ -10,6 +10,8 @@ import sys
import redis
import json
import mysql.connector
import re
import threading
from lib.datetime import filter_accounts_next_30_days, filter_accounts_expired
from lib.reqs import (get_urls, get_user_accounts, add_user_account,
@ -185,9 +187,11 @@ def login() -> Union[Response, str]:
login_url, auth=requests.auth.HTTPBasicAuth(username, password)
)
response.raise_for_status()
if response.json().get("auth") == "Success":
response_data = response.json()
if response_data.get("auth") == "Success":
session["logged_in"] = True
session["username"] = username
session["username"] = response_data.get("username", username)
session["user_id"] = response_data.get("user_id")
session["auth_credentials"] = encoded_credentials
next_url = request.args.get("next")
if next_url:
@ -249,6 +253,9 @@ def add_account() -> Union[Response, str]:
base_url, session["auth_credentials"], username, password, stream
):
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
# Run the NPM config update in a background thread
thread = threading.Thread(target=_update_npm_config_in_background)
thread.start()
return redirect(url_for("user_accounts"))
return render_template(
@ -285,6 +292,9 @@ def validate_account() -> Tuple[Response, int]:
response_data = response.json()
if response_data.get("message") == "Account is valid and updated":
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
# Run the NPM config update in a background thread
thread = threading.Thread(target=_update_npm_config_in_background)
thread.start()
return jsonify(response_data), response.status_code
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 500
@ -298,29 +308,24 @@ def stream_names() -> Union[Response, str]:
return jsonify(get_stream_names(base_url, session["auth_credentials"]))
@app.route('/config', methods=['GET', 'POST'])
@app.route('/config')
def config():
"""Handles access to the configuration page."""
if request.method == 'POST':
password = request.form.get('password')
if password == app.config['CONFIG_PASSWORD']:
session['config_logged_in'] = True
return redirect(url_for('config_dashboard'))
else:
return render_template('config.html', error='Invalid password')
return render_template('config.html')
if session.get('user_id') and int(session.get('user_id')) == 1:
return redirect(url_for('config_dashboard'))
return redirect(url_for('home'))
@app.route('/config/dashboard')
def config_dashboard():
"""Renders the configuration dashboard."""
if not session.get('config_logged_in'):
return redirect(url_for('config'))
if not session.get('user_id') or int(session.get('user_id')) != 1:
return redirect(url_for('home'))
return render_template('config_dashboard.html')
@app.route('/check-expiring-accounts', methods=['POST'])
def check_expiring_accounts():
"""Proxies the request to check for expiring accounts to the backend."""
if not session.get("config_logged_in"):
if not session.get('user_id') or int(session.get('user_id')) != 1:
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/check-expiry"
@ -334,7 +339,7 @@ def check_expiring_accounts():
@app.route('/dns', methods=['GET', 'POST', 'DELETE'])
def proxy_dns():
"""Proxies DNS management requests to the backend."""
if not session.get("config_logged_in"):
if not session.get('user_id') or int(session.get('user_id')) != 1:
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/dns"
@ -362,7 +367,7 @@ def proxy_dns():
@app.route('/extra_urls', methods=['GET', 'POST', 'DELETE'])
def proxy_extra_urls():
"""Proxies extra URL management requests to the backend."""
if not session.get("config_logged_in"):
if not session.get('user_id') or int(session.get('user_id')) != 1:
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/extra_urls"
@ -387,22 +392,158 @@ def proxy_extra_urls():
return jsonify({"error": str(e)}), 502
@app.route('/extra_urls_file', methods=['GET'])
def proxy_extra_urls_file():
"""Proxies the request to get the extra_urls.txt file from the backend."""
if not session.get("config_logged_in"):
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
def _update_npm_config():
"""Helper function to update the NPM config."""
if not session.get('user_id') or int(session.get('user_id')) != 1:
print("Unauthorized attempt to update NPM config.")
return
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:
print(f"Failed to fetch streams from backend: {e}")
return
if streams:
new_config = update_config_with_streams(current_config, streams)
npm.update_proxy_host_config(9, new_config)
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
backend_url = f"{app.config['BACKEND_URL']}/extra_urls_file"
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)
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
thread = threading.Thread(target=_update_npm_config_in_background)
thread.start()
return jsonify({'message': 'NPM config update started in the background.'}), 202
if __name__ == "__main__":

View File

@ -46,7 +46,7 @@
</main>
<footer class="bg-dark text-white text-center py-3 mt-5">
<p>Version: <a href="{{ url_for('config') }}" style="color: inherit; text-decoration: none;">{{ version }}</a></p>
<p>Version: {% if session.user_id and session.user_id|int == 1 %}<a href="{{ url_for('config') }}" style="color: inherit; text-decoration: none;">{{ version }}</a>{% else %}{{ version }}{% endif %}</p>
</footer>
<input type="hidden" id="is-logged-in" value="{{ 'true' if session.get('logged_in') else 'false' }}">

View File

@ -1,27 +0,0 @@
{% extends "base.html" %}
{% block title %}Config Access{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="text-center">Enter Password</h3>
</div>
<div class="card-body">
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<form method="post" action="{{ url_for('config') }}">
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Login</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -16,6 +16,7 @@
<button id="send-test-notification-btn" class="btn btn-primary">Send Test Notification</button>
<button id="check-expiring-accounts-btn" class="btn btn-info">Check Expiring Accounts</button>
<button id="force-resubscribe-btn" class="btn btn-warning">Force Re-subscribe</button>
<button id="update-host-9-btn" class="btn btn-success">Update Redirect URLS</button>
</div>
</div>
</div>
@ -60,7 +61,6 @@
<input type="text" class="form-control" id="extra-url-input" placeholder="Enter Extra URL">
<div class="input-group-append">
<button class="btn btn-primary" id="add-extra-url-btn">Add</button>
<button class="btn btn-secondary" id="view-extra-urls-file-btn">View extra_urls.txt</button>
</div>
</div>
<table class="table table-striped">
@ -251,11 +251,6 @@
addExtraUrlBtn.addEventListener('click', addExtraUrl);
fetchExtraUrlsList();
const viewExtraUrlsFileBtn = document.getElementById('view-extra-urls-file-btn');
viewExtraUrlsFileBtn.addEventListener('click', () => {
window.open("{{ url_for('proxy_extra_urls_file') }}", '_blank');
});
// Other buttons
document.getElementById('send-test-notification-btn').addEventListener('click', function() {
fetch('{{ url_for("send_test_notification") }}', {
@ -286,6 +281,21 @@
alert('An error occurred while triggering the expiring accounts check.');
});
});
document.getElementById('update-host-9-btn').addEventListener('click', function() {
fetch('{{ url_for("update_host_9_config") }}', {
method: 'POST'
}).then(response => {
if (response.ok) {
alert('Host 9 config updated successfully!');
} else {
alert('Failed to update Host 9 config.');
}
}).catch(err => {
console.error('Error updating Host 9 config:', err);
alert('An error occurred while updating the Host 9 config.');
});
});
});
</script>
{% endblock %}

164
test_npm_update.py Normal file
View File

@ -0,0 +1,164 @@
import requests
import json
import mysql.connector
import re
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Assuming config.py is in the same directory or accessible via PYTHONPATH
from config import DevelopmentConfig as app_config
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 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 DISTINCT
SUBSTRING_INDEX(stream, ' ', 1) AS streamName,
streamURL
FROM userAccounts
""")
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):
# 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
def main():
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', '')
print("Current Config:")
print(current_config)
streams = get_streams_from_db(app_config.DBHOST, app_config.DBUSER, app_config.DBPASS, app_config.DATABASE, app_config.DBPORT)
if streams:
new_config = update_config_with_streams(current_config, streams)
print("\nNew Config:")
print(new_config)
# Uncomment the following line to apply the changes
npm.update_proxy_host_config(9, new_config)
print("\nTo apply the changes, uncomment the last line in the main function.")
if __name__ == "__main__":
main()