From 42d2d304b2cb2810fdb13193d5e27ac41cba5b4d Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:45:57 +0330 Subject: [PATCH 01/38] Create singbox.json --- core/scripts/singbox/singbox.json | 217 ++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 core/scripts/singbox/singbox.json diff --git a/core/scripts/singbox/singbox.json b/core/scripts/singbox/singbox.json new file mode 100644 index 0000000..2f9b8aa --- /dev/null +++ b/core/scripts/singbox/singbox.json @@ -0,0 +1,217 @@ +{ + "log": { + "level": "warn", + "output": "box.log", + "timestamp": true + }, + "dns": { + "servers": [ + { + "tag": "dns-remote", + "address": "udp://1.1.1.1", + "address_resolver": "dns-direct" + }, + { + "tag": "dns-direct", + "address": "1.1.1.1", + "address_resolver": "dns-local", + "detour": "direct" + }, + { + "tag": "dns-local", + "address": "local", + "detour": "direct" + }, + { + "tag": "dns-block", + "address": "rcode://success" + } + ], + "rules": [ + { + "domain": "cp.cloudflare.com", + "server": "dns-remote", + "rewrite_ttl": 3000 + } + ], + "independent_cache": true + }, + "inbounds": [ + { + "type": "tun", + "tag": "tun-in", + "mtu": 9000, + "inet4_address": "172.19.0.1/28", + "auto_route": true, + "strict_route": true, + "endpoint_independent_nat": true, + "stack": "mixed", + "sniff": true, + "sniff_override_destination": true + }, + { + "type": "mixed", + "tag": "mixed-in", + "listen": "127.0.0.1", + "listen_port": 2334, + "sniff": true, + "sniff_override_destination": true + }, + { + "type": "direct", + "tag": "dns-in", + "listen": "127.0.0.1", + "listen_port": 6450 + } + ], + "outbounds": [ + { + "type": "selector", + "tag": "select", + "outbounds": [ + "auto", + "{username}-Hysteria2" + ], + "default": "auto" + }, + { + "type": "urltest", + "tag": "auto", + "outbounds": [ + "{username}-Hysteria2" + ], + "url": "http://connectivitycheck.gstatic.com/generate_204", + "interval": "10m0s", + "idle_timeout": "1h40m0s" + }, + { + "type": "hysteria2", + "tag": "{username}-Hysteria2", + "server": "{ip}", + "server_port": "{port}", + "obfs": { + "type": "salamander", + "password": "{obfs_password}" + }, + "password": "{username}:{password}", + "tls": { + "enabled": true, + "server_name": "bts.com", + "insecure": true + } + }, + { + "type": "dns", + "tag": "dns-out" + }, + { + "type": "direct", + "tag": "direct" + }, + { + "type": "direct", + "tag": "bypass" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "rule_set": [ + "geoip-ir", + "geosite-ir" + ], + "outbound": "direct" + }, + { + "inbound": "dns-in", + "outbound": "dns-out" + }, + { + "port": 53, + "outbound": "dns-out" + }, + { + "clash_mode": "Direct", + "outbound": "direct" + }, + { + "clash_mode": "Global", + "outbound": "select" + } + ], + "rule_set": [ + { + "type": "remote", + "tag": "geosite-ir", + "format": "binary", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs", + "download_detour": "direct", + "update_interval": "72h0m0s" + }, + { + "type": "remote", + "tag": "geosite-category-ads-all", + "format": "binary", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs", + "download_detour": "direct", + "update_interval": "72h0m0s" + }, + { + "type": "remote", + "tag": "geosite-malware", + "format": "binary", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-malware.srs", + "download_detour": "direct", + "update_interval": "72h0m0s" + }, + { + "type": "remote", + "tag": "geosite-phishing", + "format": "binary", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-phishing.srs", + "download_detour": "direct", + "update_interval": "72h0m0s" + }, + { + "type": "remote", + "tag": "geosite-cryptominers", + "format": "binary", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-cryptominers.srs", + "download_detour": "direct", + "update_interval": "72h0m0s" + }, + { + "type": "remote", + "tag": "geoip-ir", + "format": "binary", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs", + "download_detour": "direct", + "update_interval": "72h0m0s" + }, + { + "type": "remote", + "tag": "geoip-malware", + "format": "binary", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-malware.srs", + "download_detour": "direct", + "update_interval": "72h0m0s" + }, + { + "type": "remote", + "tag": "geoip-phishing", + "format": "binary", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-phishing.srs", + "download_detour": "direct", + "update_interval": "72h0m0s" + } + ], + "final": "select", + "auto_detect_interface": true, + "override_android_vpn": true + } + } + From d7299d46b36a9e77e7c3584e47d458e12db89833 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:15:24 +0330 Subject: [PATCH 02/38] Create singbox.py --- core/scripts/singbox/singbox.py | 111 ++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 core/scripts/singbox/singbox.py diff --git a/core/scripts/singbox/singbox.py b/core/scripts/singbox/singbox.py new file mode 100644 index 0000000..111d577 --- /dev/null +++ b/core/scripts/singbox/singbox.py @@ -0,0 +1,111 @@ +from aiohttp import web +import ssl +import json +import subprocess +from urllib.parse import unquote, parse_qs +import re + +async def handle(request): + username = request.query.get('username') + ip_version = request.query.get('ip') + fragment = request.match_info.get('fragment', '') + + if not username: + return web.Response(status=400, text="Error: Missing 'username' parameter.") + + if not ip_version: + return web.Response(status=400, text="Error: Missing 'ip' parameter.") + + if ip_version not in ['4', '6']: + return web.Response(status=400, text="Error: Invalid 'ip' parameter. Must be '4' or '6'.") + + try: + config = generate_singbox_config(username, ip_version, fragment) + config_json = json.dumps(config, indent=4, sort_keys=True) + + return web.Response(text=config_json, content_type='application/json') + except Exception as e: + return web.Response(status=500, text="Error: Internal server error.") + +def generate_singbox_config(username, ip_version, fragment): + # CLI command + try: + uri = subprocess.check_output([ + 'python3', '/etc/hysteria/core/cli.py', 'show-user-uri', '-u', username, '-ip', ip_version + ]).decode().strip() + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to get URI: {e}") + + if ip_version == '4': + components = extract_uri_components(uri, 'IPv4:') + else: + components = extract_uri_components(uri, 'IPv6:') + + config = load_singbox_template() + hysteria_tag = f"{username}-Hysteria2" + config['outbounds'][2]['tag'] = hysteria_tag + config['outbounds'][2]['server'] = components['ip'] + config['outbounds'][2]['server_port'] = int(components['port']) + config['outbounds'][2]['obfs']['password'] = components['obfs_password'] + config['outbounds'][2]['password'] = f"{username}:{components['password']}" + + if fragment: + config['outbounds'][2]['tls']['server_name'] = fragment + + config['outbounds'][0]['outbounds'] = ["auto", hysteria_tag] + config['outbounds'][1]['outbounds'] = [hysteria_tag] + + return config + +def extract_uri_components(uri, prefix): + if uri.startswith(prefix): + uri = uri[len(prefix):].strip() + + decoded_uri = unquote(uri) + pattern = re.compile( + r'^hy2://([^:]+):([^@]+)@(\[?[^\]]+?\]?):(\d+)\?([^#]+)(?:#([^/]+))?$' + ) + match = pattern.match(decoded_uri) + + if not match: + raise ValueError(f"Could not parse URI: {decoded_uri}") + + username = match.group(1) + password = match.group(2) + ip = match.group(3) + port = match.group(4) + query_params = match.group(5) + fragment = match.group(6) + + if ip.startswith('[') and ip.endswith(']'): + ip = ip[1:-1] + + params = parse_qs(query_params) + obfs_password = params.get('obfs-password', [''])[0] + + return { + 'username': username, + 'password': password, + 'ip': ip, + 'port': port, + 'obfs_password': obfs_password, + } + +def load_singbox_template(): + try: + with open('/etc/hysteria/core/scripts/singbox/singbox.json', 'r') as f: + return json.load(f) + except IOError as e: + raise RuntimeError(f"Failed to load template: {e}") + +if __name__ == '__main__': + app = web.Application() + app.add_routes([web.get('/sub/singbox/', handle)]) + + # SSL context + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(certfile="/etc/letsencrypt/live/example.com/fullchain.pem", keyfile="/etc/letsencrypt/live/example.com/privkey.pem") + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + ssl_context.set_ciphers('AES256+EECDH:AES256+EDH') + + web.run_app(app, port=3324, ssl_context=ssl_context) From 64a089cef96ce716161235f18a1c487b8e2ce6ed Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:18:00 +0330 Subject: [PATCH 03/38] Environment variables --- core/scripts/singbox/singbox.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/core/scripts/singbox/singbox.py b/core/scripts/singbox/singbox.py index 111d577..c670d71 100644 --- a/core/scripts/singbox/singbox.py +++ b/core/scripts/singbox/singbox.py @@ -1,9 +1,19 @@ -from aiohttp import web +import os import ssl import json import subprocess +from aiohttp import web from urllib.parse import unquote, parse_qs import re +from dotenv import load_dotenv + +load_dotenv() + +# Environment variables +DOMAIN = os.getenv('DOMAIN') +CERTFILE = os.getenv('CERTFILE') +KEYFILE = os.getenv('KEYFILE') +PORT = int(os.getenv('PORT', '3324')) async def handle(request): username = request.query.get('username') @@ -25,10 +35,9 @@ async def handle(request): return web.Response(text=config_json, content_type='application/json') except Exception as e: - return web.Response(status=500, text="Error: Internal server error.") + return web.Response(status=500, text=f"Error: {str(e)}") def generate_singbox_config(username, ip_version, fragment): - # CLI command try: uri = subprocess.check_output([ 'python3', '/etc/hysteria/core/cli.py', 'show-user-uri', '-u', username, '-ip', ip_version @@ -102,10 +111,9 @@ if __name__ == '__main__': app = web.Application() app.add_routes([web.get('/sub/singbox/', handle)]) - # SSL context ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - ssl_context.load_cert_chain(certfile="/etc/letsencrypt/live/example.com/fullchain.pem", keyfile="/etc/letsencrypt/live/example.com/privkey.pem") - ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + ssl_context.load_cert_chain(certfile=CERTFILE, keyfile=KEYFILE) + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_1 ssl_context.set_ciphers('AES256+EECDH:AES256+EDH') - web.run_app(app, port=3324, ssl_context=ssl_context) + web.run_app(app, port=PORT, ssl_context=ssl_context) From 3bfd4a9652218c722ff3348593bccd52dc5a7387 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:20:09 +0330 Subject: [PATCH 04/38] TLSv1_2 --- core/scripts/singbox/singbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scripts/singbox/singbox.py b/core/scripts/singbox/singbox.py index c670d71..6d173bb 100644 --- a/core/scripts/singbox/singbox.py +++ b/core/scripts/singbox/singbox.py @@ -113,7 +113,7 @@ if __name__ == '__main__': ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_context.load_cert_chain(certfile=CERTFILE, keyfile=KEYFILE) - ssl_context.minimum_version = ssl.TLSVersion.TLSv1_1 + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 ssl_context.set_ciphers('AES256+EECDH:AES256+EDH') web.run_app(app, port=PORT, ssl_context=ssl_context) From aaf31a79f23360ae1eb195521328923a85f847e6 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:11:16 +0330 Subject: [PATCH 05/38] Additional Security --- core/scripts/singbox/singbox.py | 89 ++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/core/scripts/singbox/singbox.py b/core/scripts/singbox/singbox.py index 6d173bb..3b22879 100644 --- a/core/scripts/singbox/singbox.py +++ b/core/scripts/singbox/singbox.py @@ -3,8 +3,11 @@ import ssl import json import subprocess from aiohttp import web +from aiohttp.web_middlewares import middleware from urllib.parse import unquote, parse_qs import re +import time + from dotenv import load_dotenv load_dotenv() @@ -15,35 +18,66 @@ CERTFILE = os.getenv('CERTFILE') KEYFILE = os.getenv('KEYFILE') PORT = int(os.getenv('PORT', '3324')) +RATE_LIMIT = 100 +RATE_LIMIT_WINDOW = 60 + +rate_limit_store = {} + +@middleware +async def rate_limit_middleware(request, handler): + client_ip = request.headers.get('X-Forwarded-For', request.remote) + current_time = time.time() + + if client_ip in rate_limit_store: + requests, last_request_time = rate_limit_store[client_ip] + if current_time - last_request_time < RATE_LIMIT_WINDOW: + if requests >= RATE_LIMIT: + return web.Response(status=429, text="Rate limit exceeded.") + if current_time - last_request_time >= RATE_LIMIT_WINDOW: + rate_limit_store[client_ip] = (1, current_time) + else: + rate_limit_store[client_ip] = (requests + 1, last_request_time) + else: + rate_limit_store[client_ip] = (1, current_time) + + return await handler(request) + +def sanitize_input(value, pattern): + if not re.match(pattern, value): + raise ValueError(f"Invalid value: {value}") + return value + async def handle(request): - username = request.query.get('username') - ip_version = request.query.get('ip') - fragment = request.match_info.get('fragment', '') - - if not username: - return web.Response(status=400, text="Error: Missing 'username' parameter.") - - if not ip_version: - return web.Response(status=400, text="Error: Missing 'ip' parameter.") - - if ip_version not in ['4', '6']: - return web.Response(status=400, text="Error: Invalid 'ip' parameter. Must be '4' or '6'.") - try: + username = sanitize_input(request.match_info.get('username', ''), r'^[a-zA-Z0-9_-]+$') + ip_version = sanitize_input(request.match_info.get('ip_version', ''), r'^[46]$') + fragment = request.query.get('fragment', '') + + if not username: + return web.Response(status=400, text="Error: Missing 'username' parameter.") + + if not ip_version: + return web.Response(status=400, text="Error: Missing 'ip' parameter.") + + if ip_version not in ['4', '6']: + return web.Response(status=400, text="Error: Invalid 'ip' parameter. Must be '4' or '6'.") + config = generate_singbox_config(username, ip_version, fragment) config_json = json.dumps(config, indent=4, sort_keys=True) return web.Response(text=config_json, content_type='application/json') + except ValueError as e: + return web.Response(status=400, text=f"Error: {str(e)}") except Exception as e: - return web.Response(status=500, text=f"Error: {str(e)}") + print(f"Internal Server Error: {str(e)}") + return web.Response(status=500, text="Error: Internal server error.") def generate_singbox_config(username, ip_version, fragment): try: - uri = subprocess.check_output([ - 'python3', '/etc/hysteria/core/cli.py', 'show-user-uri', '-u', username, '-ip', ip_version - ]).decode().strip() - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Failed to get URI: {e}") + command = ['python3', '/etc/hysteria/core/cli.py', 'show-user-uri', '-u', username, '-ip', ip_version] + uri = subprocess.check_output(command).decode().strip() + except subprocess.CalledProcessError: + raise RuntimeError("Failed to get URI.") if ip_version == '4': components = extract_uri_components(uri, 'IPv4:') @@ -77,7 +111,7 @@ def extract_uri_components(uri, prefix): match = pattern.match(decoded_uri) if not match: - raise ValueError(f"Could not parse URI: {decoded_uri}") + raise ValueError("Could not parse URI.") username = match.group(1) password = match.group(2) @@ -104,12 +138,19 @@ def load_singbox_template(): try: with open('/etc/hysteria/core/scripts/singbox/singbox.json', 'r') as f: return json.load(f) - except IOError as e: - raise RuntimeError(f"Failed to load template: {e}") + except IOError: + raise RuntimeError("Failed to load template.") + +async def handle_404(request): + print(f"404 Not Found: {request.path}") + return web.Response(status=404, text="Not Found") + if __name__ == '__main__': - app = web.Application() - app.add_routes([web.get('/sub/singbox/', handle)]) + app = web.Application(middlewares=[rate_limit_middleware]) + + app.add_routes([web.get('/sub/singbox/{username}/{ip_version}', handle)]) + app.router.add_route('*', '/sub/singbox/{tail:.*}', handle_404) ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_context.load_cert_chain(certfile=CERTFILE, keyfile=KEYFILE) From 438307e908c166df67033f3f6cc669458df3b112 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:31:44 +0330 Subject: [PATCH 06/38] Extract traffic data --- core/scripts/hysteria2/get_user.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/core/scripts/hysteria2/get_user.sh b/core/scripts/hysteria2/get_user.sh index d67b4c1..026c7e2 100644 --- a/core/scripts/hysteria2/get_user.sh +++ b/core/scripts/hysteria2/get_user.sh @@ -2,8 +2,6 @@ source /etc/hysteria/core/scripts/path.sh - -# Check if a username is provided if [ -z "$1" ]; then echo "Usage: $0 " exit 1 @@ -11,22 +9,33 @@ fi USERNAME=$1 -# Check if users.json file exists if [ ! -f "$USERS_FILE" ]; then echo "users.json file not found!" exit 1 fi -# Extract user info using jq +if [ ! -f "$TRAFFIC_FILE" ]; then + echo "traffic_data.json file not found!" + exit 1 +fi + USER_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' $USERS_FILE) -# Check if user info is found if [ -z "$USER_INFO" ]; then echo "User '$USERNAME' not found." exit 1 fi -# Print user info +TRAFFIC_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' $TRAFFIC_FILE) + +echo "User Information:" echo "$USER_INFO" | jq . +echo "Traffic Information:" +if [ -z "$TRAFFIC_INFO" ]; then + echo "No traffic data found for user '$USERNAME'." +else + echo "$TRAFFIC_INFO" | jq . +fi + exit 0 From eab1e312963ea09d822447f73ae4d5827fe82fae Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:33:14 +0330 Subject: [PATCH 07/38] Extract and handle traffic data --- core/scripts/telegrambot/tbot.py | 92 +++++++++++++++++++------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/core/scripts/telegrambot/tbot.py b/core/scripts/telegrambot/tbot.py index e8abd5f..ef00763 100644 --- a/core/scripts/telegrambot/tbot.py +++ b/core/scripts/telegrambot/tbot.py @@ -4,6 +4,7 @@ import qrcode import io import json import os +import re from dotenv import load_dotenv from telebot import types @@ -87,52 +88,67 @@ def show_user(message): def process_show_user(message): username = message.text.strip() + command = f"python3 {CLI_PATH} get-user -u {username}" - result = run_cli_command(command) + user_result = run_cli_command(command) - if "Error" in result or "Invalid" in result: - bot.reply_to(message, result) - else: - user_details = json.loads(result) - formatted_details = ( - f"Name: {username}\n" - f"Traffic limit: {user_details['max_download_bytes'] / (1024 ** 3):.2f} GB\n" - f"Days: {user_details['expiration_days']}\n" - f"Account Creation: {user_details['account_creation_date']}\n" - f"Blocked: {user_details['blocked']}" - ) + user_json_match = re.search(r'User Information:\s*(\{.*?\})\s*Traffic Information:\s*(\{.*?\})', user_result, re.DOTALL) + + if not user_json_match: + bot.reply_to(message, "Failed to parse user details. The command output format may be incorrect.") + return - qr_command = f"python3 {CLI_PATH} show-user-uri -u {username} -ip 4" - qr_result = run_cli_command(qr_command) + user_json, traffic_json = user_json_match.groups() - if "Error" in qr_result or "Invalid" in qr_result: - bot.reply_to(message, qr_result) - return - uri_v4 = qr_result.split('\n')[-1].strip() + try: + user_details = json.loads(user_json) + traffic_data = json.loads(traffic_json) + except json.JSONDecodeError: + bot.reply_to(message, "Failed to parse JSON data. The command output may be malformed.") + return - qr_v4 = qrcode.make(uri_v4) - bio_v4 = io.BytesIO() - qr_v4.save(bio_v4, 'PNG') - bio_v4.seek(0) + formatted_details = ( + f"Name: {username}\n" + f"Traffic Limit: {user_details['max_download_bytes'] / (1024 ** 3):.2f} GB\n" + f"Days: {user_details['expiration_days']}\n" + f"Account Creation: {user_details['account_creation_date']}\n" + f"Blocked: {user_details['blocked']}\n\n" + f"**Traffic Data:**\n" + f"Upload: {traffic_data.get('upload_bytes', 0) / (1024 ** 2):.2f} MB\n" + f"Download: {traffic_data.get('download_bytes', 0) / (1024 ** 2):.2f} MB\n" + f"Status: {traffic_data.get('status', 'Unknown')}" + ) - markup = types.InlineKeyboardMarkup(row_width=3) - markup.add(types.InlineKeyboardButton("Reset User", callback_data=f"reset_user:{username}"), - types.InlineKeyboardButton("IPv6-URI", callback_data=f"ipv6_uri:{username}")) - markup.add(types.InlineKeyboardButton("Edit Username", callback_data=f"edit_username:{username}"), - types.InlineKeyboardButton("Edit Traffic Limit", callback_data=f"edit_traffic:{username}")) - markup.add(types.InlineKeyboardButton("Edit Expiration Days", callback_data=f"edit_expiration:{username}"), - types.InlineKeyboardButton("Renew Password", callback_data=f"renew_password:{username}")) - markup.add(types.InlineKeyboardButton("Renew Creation Date", callback_data=f"renew_creation:{username}"), - types.InlineKeyboardButton("Block User", callback_data=f"block_user:{username}")) + qr_command = f"python3 {CLI_PATH} show-user-uri -u {username} -ip 4" + qr_result = run_cli_command(qr_command) - bot.send_photo( - message.chat.id, - bio_v4, - caption=f"**User Details:**\n\n{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`", - reply_markup=markup, - parse_mode="Markdown" - ) + if "Error" in qr_result or "Invalid" in qr_result: + bot.reply_to(message, qr_result) + return + uri_v4 = qr_result.split('\n')[-1].strip() + qr_v4 = qrcode.make(uri_v4) + bio_v4 = io.BytesIO() + qr_v4.save(bio_v4, 'PNG') + bio_v4.seek(0) + + markup = types.InlineKeyboardMarkup(row_width=3) + markup.add(types.InlineKeyboardButton("Reset User", callback_data=f"reset_user:{username}"), + types.InlineKeyboardButton("IPv6-URI", callback_data=f"ipv6_uri:{username}")) + markup.add(types.InlineKeyboardButton("Edit Username", callback_data=f"edit_username:{username}"), + types.InlineKeyboardButton("Edit Traffic Limit", callback_data=f"edit_traffic:{username}")) + markup.add(types.InlineKeyboardButton("Edit Expiration Days", callback_data=f"edit_expiration:{username}"), + types.InlineKeyboardButton("Renew Password", callback_data=f"renew_password:{username}")) + markup.add(types.InlineKeyboardButton("Renew Creation Date", callback_data=f"renew_creation:{username}"), + types.InlineKeyboardButton("Block User", callback_data=f"block_user:{username}")) + + bot.send_photo( + message.chat.id, + bio_v4, + caption=f"**User Details:**\n\n{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`", + reply_markup=markup, + parse_mode="Markdown" + ) @bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'Server Info') def server_info(message): command = f"python3 {CLI_PATH} server-info" From b15a23629a17654e26d66b0e6c5847108e68b9d3 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:40:51 +0330 Subject: [PATCH 08/38] Fix Bug --- core/scripts/telegrambot/tbot.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/core/scripts/telegrambot/tbot.py b/core/scripts/telegrambot/tbot.py index ef00763..0c28f43 100644 --- a/core/scripts/telegrambot/tbot.py +++ b/core/scripts/telegrambot/tbot.py @@ -91,32 +91,39 @@ def process_show_user(message): command = f"python3 {CLI_PATH} get-user -u {username}" user_result = run_cli_command(command) - - user_json_match = re.search(r'User Information:\s*(\{.*?\})\s*Traffic Information:\s*(\{.*?\})', user_result, re.DOTALL) + user_json_match = re.search(r'User Information:\s*(\{.*?\})\s*(Traffic Information:\s*(\{.*?\}|No traffic data found.*))?', user_result, re.DOTALL) if not user_json_match: bot.reply_to(message, "Failed to parse user details. The command output format may be incorrect.") return - user_json, traffic_json = user_json_match.groups() + user_json = user_json_match.group(1) + traffic_data_section = user_json_match.group(3) try: user_details = json.loads(user_json) - traffic_data = json.loads(traffic_json) + if traffic_data_section and "No traffic data found" not in traffic_data_section: + traffic_data = json.loads(traffic_data_section) + traffic_message = ( + f"**Traffic Data:**\n" + f"Upload: {traffic_data.get('upload_bytes', 0) / (1024 ** 2):.2f} MB\n" + f"Download: {traffic_data.get('download_bytes', 0) / (1024 ** 2):.2f} MB\n" + f"Status: {traffic_data.get('status', 'Unknown')}" + ) + else: + traffic_message = "No traffic data available.\nUser might be on hold or data is not yet available." except json.JSONDecodeError: bot.reply_to(message, "Failed to parse JSON data. The command output may be malformed.") return formatted_details = ( + f"**User Details:**\n\n" f"Name: {username}\n" f"Traffic Limit: {user_details['max_download_bytes'] / (1024 ** 3):.2f} GB\n" f"Days: {user_details['expiration_days']}\n" f"Account Creation: {user_details['account_creation_date']}\n" f"Blocked: {user_details['blocked']}\n\n" - f"**Traffic Data:**\n" - f"Upload: {traffic_data.get('upload_bytes', 0) / (1024 ** 2):.2f} MB\n" - f"Download: {traffic_data.get('download_bytes', 0) / (1024 ** 2):.2f} MB\n" - f"Status: {traffic_data.get('status', 'Unknown')}" + f"{traffic_message}" ) qr_command = f"python3 {CLI_PATH} show-user-uri -u {username} -ip 4" @@ -145,10 +152,11 @@ def process_show_user(message): bot.send_photo( message.chat.id, bio_v4, - caption=f"**User Details:**\n\n{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`", + caption=f"{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`", reply_markup=markup, parse_mode="Markdown" ) + @bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'Server Info') def server_info(message): command = f"python3 {CLI_PATH} server-info" From 2acfa9107766e09dffb3492890278fc81c17dd36 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:14:04 +0330 Subject: [PATCH 09/38] Added env --- core/scripts/path.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/scripts/path.sh b/core/scripts/path.sh index d44a4b6..8c0b5fc 100644 --- a/core/scripts/path.sh +++ b/core/scripts/path.sh @@ -1,4 +1,6 @@ CLI_PATH="/etc/hysteria/core/cli.py" USERS_FILE="/etc/hysteria/users.json" TRAFFIC_FILE="/etc/hysteria/traffic_data.json" -CONFIG_FILE="/etc/hysteria/config.json" \ No newline at end of file +CONFIG_FILE="/etc/hysteria/config.json" +TELEGRAM_ENV="/etc/hysteria/core/scripts/telegrambot/.env" +SINGBOX_ENV="/etc/hysteria/core/scripts/singbox/.env" From fa255dbadfceb75ff448e661ea636d9287b334d7 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:19:09 +0330 Subject: [PATCH 10/38] Create singbox_shell.sh --- core/scripts/singbox/singbox_shell.sh | 106 ++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 core/scripts/singbox/singbox_shell.sh diff --git a/core/scripts/singbox/singbox_shell.sh b/core/scripts/singbox/singbox_shell.sh new file mode 100644 index 0000000..2de4f06 --- /dev/null +++ b/core/scripts/singbox/singbox_shell.sh @@ -0,0 +1,106 @@ +#!/bin/bash +source /etc/hysteria/core/scripts/utils.sh +define_colors + +install_dependencies() { + echo "Installing necessary dependencies..." + apt-get install certbot -y > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo -e "${red}Error: Failed to install dependencies. ${NC}" + exit 1 + fi + echo -e "${green}Dependencies installed successfully. ${NC}" +} + +update_env_file() { + local domain=$1 + local port=$2 + local cert_dir="/etc/letsencrypt/live/$domain" + + cat < /etc/hysteria/core/scripts/singbox/.env +DOMAIN=$domain +PORT=$port +CERTFILE=$cert_dir/fullchain.pem +KEYFILE=$cert_dir/privkey.pem +EOL +} + +create_service_file() { + cat < /etc/systemd/system/singbox.service +[Unit] +Description=Singbox Python Service +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /etc/hysteria/core/scripts/singbox/singbox.py +WorkingDirectory=/etc/hysteria/core/scripts/singbox +EnvironmentFile=/etc/hysteria/core/scripts/singbox/.env +Restart=always +User=root +Group=root + +[Install] +WantedBy=multi-user.target +EOL +} + +start_service() { + local domain=$1 + local port=$2 + + if systemctl is-active --quiet singbox.service; then + echo "The singbox.service is already running." + return + fi + + install_dependencies + + echo "Generating SSL certificates for $domain..." + certbot certonly --standalone --agree-tos --register-unsafely-without-email -d "$domain" + if [ $? -ne 0 ]; then + echo -e "${red}Error: Failed to generate SSL certificates. ${NC}" + exit 1 + fi + + update_env_file "$domain" "$port" + create_service_file + + systemctl daemon-reload + systemctl enable singbox.service > /dev/null 2>&1 + systemctl start singbox.service > /dev/null 2>&1 + + if systemctl is-active --quiet singbox.service; then + echo -e "${green}Singbox service setup completed. The service is now running on port $port. ${NC}" + else + echo -e "${red}Singbox setup completed. The service failed to start. ${NC}" + fi +} + +stop_service() { + systemctl stop singbox.service > /dev/null 2>&1 + systemctl disable singbox.service > /dev/null 2>&1 + + rm -f /etc/hysteria/core/scripts/singbox/.env + echo -e "\n" + + echo -e "${yellow}Singbox service stopped and disabled. .env file removed. ${NC}" +} + +case "$1" in + start) + if [ -z "$2" ] || [ -z "$3" ]; then + echo -e "${red}Usage: $0 start ${NC}" + exit 1 + fi + start_service "$2" "$3" + ;; + stop) + stop_service + ;; + *) + echo -e "${red}Usage: $0 {start|stop} ${NC}" + exit 1 + ;; +esac + +define_colors From 71dcd9af75a2e8d00ace93b93a20d7e4f7709502 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:21:35 +0330 Subject: [PATCH 11/38] SingBox Sub --- core/cli.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/core/cli.py b/core/cli.py index a842129..c98441f 100644 --- a/core/cli.py +++ b/core/cli.py @@ -32,6 +32,7 @@ class Command(Enum): LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.sh') SERVER_INFO = os.path.join(SCRIPT_DIR, 'hysteria2', 'server_info.sh') INSTALL_TELEGRAMBOT = os.path.join(SCRIPT_DIR, 'telegrambot', 'runbot.sh') + INSTALL_SINGBOX = os.path.join(SCRIPT_DIR, 'singbox', 'singbox_shell.sh') INSTALL_TCP_BRUTAL = os.path.join(SCRIPT_DIR, 'tcp-brutal', 'install.sh') INSTALL_WARP = os.path.join(SCRIPT_DIR, 'warp', 'install.sh') UNINSTALL_WARP = os.path.join(SCRIPT_DIR, 'warp', 'uninstall.sh') @@ -193,7 +194,8 @@ def remove_user(username: str): @click.option('--qrcode', '-qr', is_flag=True, help='Generate QR code for the URI') @click.option('--ipv', '-ip', type=click.IntRange(4, 6), default=4, help='IP version (4 or 6)') @click.option('--all', '-a', is_flag=True, help='Show both IPv4 and IPv6 URIs and generate QR codes for both if requested') -def show_user_uri(username: str, qrcode: bool, ipv: int, all: bool): +@click.option('--singbox', '-s', is_flag=True, help='Generate Singbox sublink if Singbox service is active') +def show_user_uri(username: str, qrcode: bool, ipv: int, all: bool, singbox: bool): command_args = ['bash', Command.SHOW_USER_URI.value, '-u', username] if qrcode: command_args.append('-qr') @@ -201,10 +203,11 @@ def show_user_uri(username: str, qrcode: bool, ipv: int, all: bool): command_args.append('-a') else: command_args.extend(['-ip', str(ipv)]) + if singbox: + command_args.append('-s') run_cmd(command_args) - @ cli.command('traffic-status') def traffic_status(): traffic.traffic_status() @@ -271,6 +274,19 @@ def telegram(action: str, token: str, adminid: str): elif action == 'stop': run_cmd(['bash', Command.INSTALL_TELEGRAMBOT.value, 'stop']) +@cli.command('singbox') +@click.option('--action', '-a', required=True, help='Action to perform: start or stop', type=click.Choice(['start', 'stop'], case_sensitive=False)) +@click.option('--domain', '-d', required=False, help='Domain name for SSL', type=str) +@click.option('--port', '-p', required=False, help='Port number for Singbox service', type=int) +def singbox(action: str, domain: str, port: int): + if action == 'start': + if not domain or not port: + click.echo("Error: Both --domain and --port are required for the start action.") + return + run_cmd(['bash', Command.INSTALL_SINGBOX.value, 'start', domain, str(port)]) + elif action == 'stop': + run_cmd(['bash', Command.INSTALL_SINGBOX.value, 'stop']) + # endregion From e499470c35e67297c8ae6975668141ab643f4e25 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:27:41 +0330 Subject: [PATCH 12/38] Added Options --- core/scripts/hysteria2/get_user.sh | 49 +++++++++++++++++++----------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/core/scripts/hysteria2/get_user.sh b/core/scripts/hysteria2/get_user.sh index 026c7e2..5694c40 100644 --- a/core/scripts/hysteria2/get_user.sh +++ b/core/scripts/hysteria2/get_user.sh @@ -2,23 +2,33 @@ source /etc/hysteria/core/scripts/path.sh -if [ -z "$1" ]; then - echo "Usage: $0 " +SHOW_TRAFFIC=true + +while getopts ":u:t" opt; do + case ${opt} in + u ) + USERNAME=$OPTARG + ;; + t ) + SHOW_TRAFFIC=false + ;; + \? ) + echo "Usage: $0 -u [-t]" + exit 1 + ;; + esac +done + +if [ -z "$USERNAME" ]; then + echo "Usage: $0 -u [-t]" exit 1 fi -USERNAME=$1 - if [ ! -f "$USERS_FILE" ]; then echo "users.json file not found!" exit 1 fi -if [ ! -f "$TRAFFIC_FILE" ]; then - echo "traffic_data.json file not found!" - exit 1 -fi - USER_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' $USERS_FILE) if [ -z "$USER_INFO" ]; then @@ -26,16 +36,21 @@ if [ -z "$USER_INFO" ]; then exit 1 fi -TRAFFIC_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' $TRAFFIC_FILE) - -echo "User Information:" echo "$USER_INFO" | jq . -echo "Traffic Information:" -if [ -z "$TRAFFIC_INFO" ]; then - echo "No traffic data found for user '$USERNAME'." -else - echo "$TRAFFIC_INFO" | jq . +if [ "$SHOW_TRAFFIC" = true ]; then + if [ ! -f "$TRAFFIC_FILE" ]; then + echo "traffic_data.json file not found!" + exit 1 + fi + + TRAFFIC_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' $TRAFFIC_FILE) + + if [ -z "$TRAFFIC_INFO" ]; then + echo "No traffic data found for user '$USERNAME'." + else + echo "$TRAFFIC_INFO" | jq . + fi fi exit 0 From 5dbb4d7955ea052f52eb702cc5ed6ca83fd5a5c4 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:28:25 +0330 Subject: [PATCH 13/38] Update Get User --- core/scripts/hysteria2/edit_user.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scripts/hysteria2/edit_user.sh b/core/scripts/hysteria2/edit_user.sh index b252f11..f748e55 100644 --- a/core/scripts/hysteria2/edit_user.sh +++ b/core/scripts/hysteria2/edit_user.sh @@ -70,7 +70,7 @@ convert_blocked_status() { # Function to get user info get_user_info() { local username=$1 - python3 $CLI_PATH get-user -u "$username" + python3 $CLI_PATH get-user -u "$username" -t } # Function to update user info in JSON From 03ceffa8d9846ac741f6975521742ff19f3dfab6 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:32:17 +0330 Subject: [PATCH 14/38] Update Get User --- core/cli.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/cli.py b/core/cli.py index c98441f..947634c 100644 --- a/core/cli.py +++ b/core/cli.py @@ -98,12 +98,15 @@ def restart_hysteria2(): def change_hysteria2_port(port: int): run_cmd(['bash', Command.CHANGE_PORT_HYSTERIA2.value, str(port)]) - @cli.command('get-user') @click.option('--username', '-u', required=True, help='Username for the user to get', type=str) -def get_user(username: str): - run_cmd(['bash', Command.GET_USER.value, username]) - +@click.option('--no-traffic', '-t', is_flag=True, help='Do not display traffic information') +def get_user(username: str, no_traffic: bool): + cmd = ['bash', Command.GET_USER.value, '-u', str(username)] + if no_traffic: + cmd.append('-t') + + run_cmd(cmd) @cli.command('add-user') @click.option('--username', '-u', required=True, help='Username for the new user', type=str) From be67b999b08d4a9d8f38e85af4a810b68bc6b3e1 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:47:09 +0330 Subject: [PATCH 15/38] Fix Some Bug --- core/scripts/telegrambot/tbot.py | 50 +++++++++++++++++--------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/core/scripts/telegrambot/tbot.py b/core/scripts/telegrambot/tbot.py index 0c28f43..f5d5a5f 100644 --- a/core/scripts/telegrambot/tbot.py +++ b/core/scripts/telegrambot/tbot.py @@ -24,7 +24,7 @@ def run_cli_command(command): def create_main_markup(): markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - markup.row('Show User', 'Add User') + markup.row('Add User', 'Show User') markup.row('Delete User', 'Server Info') return markup @@ -88,30 +88,26 @@ def show_user(message): def process_show_user(message): username = message.text.strip() - command = f"python3 {CLI_PATH} get-user -u {username}" user_result = run_cli_command(command) - user_json_match = re.search(r'User Information:\s*(\{.*?\})\s*(Traffic Information:\s*(\{.*?\}|No traffic data found.*))?', user_result, re.DOTALL) + user_json_match = re.search(r'(\{.*?\})\n(\{.*?\})', user_result, re.DOTALL) if not user_json_match: bot.reply_to(message, "Failed to parse user details. The command output format may be incorrect.") return user_json = user_json_match.group(1) - traffic_data_section = user_json_match.group(3) + traffic_data_section = user_json_match.group(2) try: user_details = json.loads(user_json) - if traffic_data_section and "No traffic data found" not in traffic_data_section: - traffic_data = json.loads(traffic_data_section) - traffic_message = ( - f"**Traffic Data:**\n" - f"Upload: {traffic_data.get('upload_bytes', 0) / (1024 ** 2):.2f} MB\n" - f"Download: {traffic_data.get('download_bytes', 0) / (1024 ** 2):.2f} MB\n" - f"Status: {traffic_data.get('status', 'Unknown')}" - ) - else: - traffic_message = "No traffic data available.\nUser might be on hold or data is not yet available." + traffic_data = json.loads(traffic_data_section) + traffic_message = ( + f"**Traffic Data:**\n" + f"Upload: {traffic_data.get('upload_bytes', 0) / (1024 ** 2):.2f} MB\n" + f"Download: {traffic_data.get('download_bytes', 0) / (1024 ** 2):.2f} MB\n" + f"Status: {traffic_data.get('status', 'Unknown')}" + ) except json.JSONDecodeError: bot.reply_to(message, "Failed to parse JSON data. The command output may be malformed.") return @@ -126,14 +122,18 @@ def process_show_user(message): f"{traffic_message}" ) - qr_command = f"python3 {CLI_PATH} show-user-uri -u {username} -ip 4" - qr_result = run_cli_command(qr_command) + combined_command = f"python3 {CLI_PATH} show-user-uri -u {username} -ip 4 -s" + combined_result = run_cli_command(combined_command) - if "Error" in qr_result or "Invalid" in qr_result: - bot.reply_to(message, qr_result) + if "Error" in combined_result or "Invalid" in combined_result: + bot.reply_to(message, combined_result) return - uri_v4 = qr_result.split('\n')[-1].strip() + result_lines = combined_result.split('\n') + uri_v4 = result_lines[1].strip() + + singbox_sublink = result_lines[-1].strip() if "https://" in result_lines[-1] else None + qr_v4 = qrcode.make(uri_v4) bio_v4 = io.BytesIO() qr_v4.save(bio_v4, 'PNG') @@ -149,10 +149,14 @@ def process_show_user(message): markup.add(types.InlineKeyboardButton("Renew Creation Date", callback_data=f"renew_creation:{username}"), types.InlineKeyboardButton("Block User", callback_data=f"block_user:{username}")) + caption = f"{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`" + if singbox_sublink: + caption += f"\n\n\n**SingBox SUB:**\n{singbox_sublink}" + bot.send_photo( message.chat.id, bio_v4, - caption=f"{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`", + caption=caption, reply_markup=markup, parse_mode="Markdown" ) @@ -176,11 +180,11 @@ def handle_edit_callback(call): msg = bot.send_message(call.message.chat.id, f"Enter new expiration days for {username}:") bot.register_next_step_handler(msg, process_edit_expiration, username) elif action == 'renew_password': - command = f"python3 {CLI_PATH} edit-user -u {username} -rp" + command = f"python3 {CLI_PATH} get-user -u {username} -t" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'renew_creation': - command = f"python3 {CLI_PATH} edit-user -u {username} -rc" + command = f"python3 {CLI_PATH} get-user -u {username} -t" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'block_user': @@ -189,7 +193,7 @@ def handle_edit_callback(call): types.InlineKeyboardButton("False", callback_data=f"confirm_block:{username}:false")) bot.send_message(call.message.chat.id, f"Set block status for {username}:", reply_markup=markup) elif action == 'reset_user': - command = f"python3 {CLI_PATH} reset-user -u {username}" + command = f"python3 {CLI_PATH} get-user -u {username} -t" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'ipv6_uri': From 1c449174f2633380fdb2472fdf78db89dcb08c10 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:50:28 +0330 Subject: [PATCH 16/38] Fix Bug --- core/scripts/telegrambot/tbot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/scripts/telegrambot/tbot.py b/core/scripts/telegrambot/tbot.py index f5d5a5f..83e9272 100644 --- a/core/scripts/telegrambot/tbot.py +++ b/core/scripts/telegrambot/tbot.py @@ -180,11 +180,11 @@ def handle_edit_callback(call): msg = bot.send_message(call.message.chat.id, f"Enter new expiration days for {username}:") bot.register_next_step_handler(msg, process_edit_expiration, username) elif action == 'renew_password': - command = f"python3 {CLI_PATH} get-user -u {username} -t" + command = f"python3 {CLI_PATH} edit-user -u {username} -rp" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'renew_creation': - command = f"python3 {CLI_PATH} get-user -u {username} -t" + command = f"python3 {CLI_PATH} edit-user -u {username} -rc" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'block_user': @@ -193,7 +193,7 @@ def handle_edit_callback(call): types.InlineKeyboardButton("False", callback_data=f"confirm_block:{username}:false")) bot.send_message(call.message.chat.id, f"Set block status for {username}:", reply_markup=markup) elif action == 'reset_user': - command = f"python3 {CLI_PATH} get-user -u {username} -t" + command = f"python3 {CLI_PATH} reset-user -u {username}" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'ipv6_uri': From 5a9fb0ed8eb5b7d29d5ad5fdff410c185f224407 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 13:24:13 +0330 Subject: [PATCH 17/38] Added Singbox SubLink --- menu.sh | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/menu.sh b/menu.sh index beb2b43..2c7745a 100644 --- a/menu.sh +++ b/menu.sh @@ -304,6 +304,58 @@ telegram_bot_handler() { done } +singbox_handler() { + while true; do + echo -e "${cyan}1.${NC} Start Singbox service" + echo -e "${red}2.${NC} Stop Singbox service" + echo "0. Back" + read -p "Choose an option: " option + + case $option in + 1) + if systemctl is-active --quiet singbox.service; then + echo "The singbox.service is already active." + else + while true; do + read -p "Enter the domain name for the SSL certificate: " domain + if [ -z "$domain" ]; then + echo "Domain name cannot be empty. Please try again." + else + break + fi + done + + while true; do + read -p "Enter the port number for the service: " port + if [ -z "$port" ]; then + echo "Port number cannot be empty. Please try again." + elif ! [[ "$port" =~ ^[0-9]+$ ]]; then + echo "Port must be a number. Please try again." + else + break + fi + done + + python3 $CLI_PATH singbox -a start -d "$domain" -p "$port" + fi + ;; + 2) + if ! systemctl is-active --quiet singbox.service; then + echo "The singbox.service is already inactive." + else + python3 $CLI_PATH singbox -a stop + fi + ;; + 0) + break + ;; + *) + echo "Invalid option. Please try again." + ;; + esac + done +} + # Function to display the main menu display_main_menu() { clear @@ -411,9 +463,10 @@ display_advance_menu() { echo -e "${cyan}[3] ${NC}↝ Configure WARP" echo -e "${red}[4] ${NC}↝ Uninstall WARP" echo -e "${green}[5] ${NC}↝ Telegram Bot" - echo -e "${cyan}[6] ${NC}↝ Change Port Hysteria2" - echo -e "${cyan}[7] ${NC}↝ Update Core Hysteria2" - echo -e "${red}[8] ${NC}↝ Uninstall Hysteria2" + echo -e "${green}[6] ${NC}↝ SingBox SubLink" + echo -e "${cyan}[7] ${NC}↝ Change Port Hysteria2" + echo -e "${cyan}[8] ${NC}↝ Update Core Hysteria2" + echo -e "${red}[9] ${NC}↝ Uninstall Hysteria2" echo -e "${red}[0] ${NC}↝ Back to Main Menu" echo -e "${LPurple}◇──────────────────────────────────────────────────────────────────────◇${NC}" echo -ne "${yellow}➜ Enter your option: ${NC}" @@ -432,9 +485,10 @@ advance_menu() { 3) warp_configure_handler ;; 4) python3 $CLI_PATH uninstall-warp ;; 5) telegram_bot_handler ;; - 6) hysteria2_change_port_handler ;; - 7) python3 $CLI_PATH update-hysteria2 ;; - 8) python3 $CLI_PATH uninstall-hysteria2 ;; + 6) singbox_handler ;; + 7) hysteria2_change_port_handler ;; + 8) python3 $CLI_PATH update-hysteria2 ;; + 9) python3 $CLI_PATH uninstall-hysteria2 ;; 0) return ;; *) echo "Invalid option. Please try again." ;; esac From 9b9a226262aa6a896986524589993b9ab1ed0196 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 13:28:32 +0330 Subject: [PATCH 18/38] Backup SingBox env --- upgrade.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/upgrade.sh b/upgrade.sh index 19b96f1..3cb28f9 100644 --- a/upgrade.sh +++ b/upgrade.sh @@ -9,6 +9,7 @@ FILES=( "/etc/hysteria/traffic_data.json" "/etc/hysteria/config.json" "/etc/hysteria/core/scripts/telegrambot/.env" + "/etc/hysteria/core/scripts/singbox/.env" ) echo "Backing up files to $TEMP_DIR" From 0e1ac0ab1021a99a6d317c220822d20661cb43ba Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 13:33:50 +0330 Subject: [PATCH 19/38] Update Restarting services --- upgrade.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/upgrade.sh b/upgrade.sh index 3cb28f9..d291893 100644 --- a/upgrade.sh +++ b/upgrade.sh @@ -36,9 +36,10 @@ echo "Setting ownership and permissions for ca.key and ca.crt" chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt -echo "Restarting hysteria and bot service" +echo "Restarting hysteria services" systemctl restart hysteria-server.service systemctl restart hysteria-bot.service +systemctl restart singbox.service echo "Setting execute permissions for user.sh and kick.sh" chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh From 62cd51a67e1bb1a3639d2b2e063eda8105f82440 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:31:26 +0330 Subject: [PATCH 20/38] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 40854ac..4bddaa8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ pyTelegramBotAPI qrcode python-dotenv requests +aiohttp From 205bcdf1a623caabd8ed2c681ac69a5c4da87a46 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:32:04 +0330 Subject: [PATCH 21/38] Update dependencies --- core/scripts/singbox/singbox_shell.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/core/scripts/singbox/singbox_shell.sh b/core/scripts/singbox/singbox_shell.sh index 2de4f06..795697a 100644 --- a/core/scripts/singbox/singbox_shell.sh +++ b/core/scripts/singbox/singbox_shell.sh @@ -6,10 +6,22 @@ install_dependencies() { echo "Installing necessary dependencies..." apt-get install certbot -y > /dev/null 2>&1 if [ $? -ne 0 ]; then - echo -e "${red}Error: Failed to install dependencies. ${NC}" + echo -e "${red}Error: Failed to install certbot. ${NC}" + exit 1 + fi + echo -e "${green}Certbot installed successfully. ${NC}" + + if [ -f /etc/hysteria/requirements.txt ]; then + pip install -r /etc/hysteria/requirements.txt > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo -e "${red}Error: Failed to install Python dependencies. ${NC}" + exit 1 + fi + echo -e "${green}Python dependencies installed successfully. ${NC}" + else + echo -e "${red}Error: /etc/hysteria/requirements.txt not found. ${NC}" exit 1 fi - echo -e "${green}Dependencies installed successfully. ${NC}" } update_env_file() { From 571d59bd371f894503cb82167aa34e1a69a04d23 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:34:25 +0330 Subject: [PATCH 22/38] better security --- core/scripts/singbox/singbox.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/core/scripts/singbox/singbox.py b/core/scripts/singbox/singbox.py index 3b22879..3e21bb6 100644 --- a/core/scripts/singbox/singbox.py +++ b/core/scripts/singbox/singbox.py @@ -7,16 +7,16 @@ from aiohttp.web_middlewares import middleware from urllib.parse import unquote, parse_qs import re import time - +import shlex from dotenv import load_dotenv load_dotenv() # Environment variables -DOMAIN = os.getenv('DOMAIN') -CERTFILE = os.getenv('CERTFILE') -KEYFILE = os.getenv('KEYFILE') -PORT = int(os.getenv('PORT', '3324')) +DOMAIN = os.getenv('hysteria_DOMAIN') +CERTFILE = os.getenv('hysteria_CERTFILE') +KEYFILE = os.getenv('hysteria_KEYFILE') +PORT = int(os.getenv('hysteria_PORT', '3324')) RATE_LIMIT = 100 RATE_LIMIT_WINDOW = 60 @@ -45,7 +45,7 @@ async def rate_limit_middleware(request, handler): def sanitize_input(value, pattern): if not re.match(pattern, value): raise ValueError(f"Invalid value: {value}") - return value + return shlex.quote(value) async def handle(request): try: @@ -74,7 +74,17 @@ async def handle(request): def generate_singbox_config(username, ip_version, fragment): try: - command = ['python3', '/etc/hysteria/core/cli.py', 'show-user-uri', '-u', username, '-ip', ip_version] + username = sanitize_input(username, r'^[a-zA-Z0-9_-]+$') + ip_version = sanitize_input(ip_version, r'^[46]$') + + command = [ + 'python3', + '/etc/hysteria/core/cli.py', + 'show-user-uri', + '-u', username, + '-ip', ip_version + ] + uri = subprocess.check_output(command).decode().strip() except subprocess.CalledProcessError: raise RuntimeError("Failed to get URI.") @@ -145,7 +155,6 @@ async def handle_404(request): print(f"404 Not Found: {request.path}") return web.Response(status=404, text="Not Found") - if __name__ == '__main__': app = web.Application(middlewares=[rate_limit_middleware]) From a3b984ddc5f7ca3d61cd22204b8ba2993bc73049 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:35:23 +0330 Subject: [PATCH 23/38] Update Environment variables --- core/scripts/singbox/singbox_shell.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/scripts/singbox/singbox_shell.sh b/core/scripts/singbox/singbox_shell.sh index 795697a..708d6f5 100644 --- a/core/scripts/singbox/singbox_shell.sh +++ b/core/scripts/singbox/singbox_shell.sh @@ -30,10 +30,10 @@ update_env_file() { local cert_dir="/etc/letsencrypt/live/$domain" cat < /etc/hysteria/core/scripts/singbox/.env -DOMAIN=$domain -PORT=$port -CERTFILE=$cert_dir/fullchain.pem -KEYFILE=$cert_dir/privkey.pem +hysteria_DOMAIN=$domain +hysteria_PORT=$port +hysteria_CERTFILE=$cert_dir/fullchain.pem +hysteria_KEYFILE=$cert_dir/privkey.pem EOL } From fd1544e1e00b903ec229cfbdf84c5a08d85d4f73 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:06:59 +0330 Subject: [PATCH 24/38] Added SingBox SUB --- core/scripts/hysteria2/show_user_uri.sh | 34 +++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/core/scripts/hysteria2/show_user_uri.sh b/core/scripts/hysteria2/show_user_uri.sh index 05d7309..4549733 100644 --- a/core/scripts/hysteria2/show_user_uri.sh +++ b/core/scripts/hysteria2/show_user_uri.sh @@ -2,6 +2,17 @@ source /etc/hysteria/core/scripts/path.sh +get_singbox_domain_and_port() { + if [ -f "$SINGBOX_ENV" ]; then + local domain port + domain=$(grep -E '^hysteria_DOMAIN=' "$SINGBOX_ENV" | cut -d'=' -f2) + port=$(grep -E '^hysteria_PORT=' "$SINGBOX_ENV" | cut -d'=' -f2) + echo "$domain" "$port" + else + echo "" + fi +} + show_uri() { if [ -f "$USERS_FILE" ]; then if systemctl is-active --quiet hysteria-server.service; then @@ -9,6 +20,7 @@ show_uri() { local generate_qrcode=false local ip_version=4 local show_all=false + local generate_singbox=false while [[ "$#" -gt 0 ]]; do case $1 in @@ -16,13 +28,14 @@ show_uri() { -qr|--qrcode) generate_qrcode=true ;; -ip) ip_version="$2"; shift ;; -a|--all) show_all=true ;; + -s|--singbox) generate_singbox=true ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done if [ -z "$username" ]; then - echo "Usage: $0 -u [-qr] [-ip <4|6>] [-a]" + echo "Usage: $0 -u [-qr] [-ip <4|6>] [-a] [-s]" exit 1 fi @@ -64,21 +77,10 @@ show_uri() { fi fi - if [ "$generate_qrcode" = true ]; then - cols=$(tput cols) - if [ -n "$URI" ]; then - qr1=$(echo -n "$URI" | qrencode -t UTF8 -s 3 -m 2) - echo -e "\nIPv4 QR Code:\n" - echo "$qr1" | while IFS= read -r line; do - printf "%*s\n" $(( (${#line} + cols) / 2)) "$line" - done - fi - if [ -n "$URI6" ]; then - qr2=$(echo -n "$URI6" | qrencode -t UTF8 -s 3 -m 2) - echo -e "\nIPv6 QR Code:\n" - echo "$qr2" | while IFS= read -r line; do - printf "%*s\n" $(( (${#line} + cols) / 2)) "$line" - done + if [ "$generate_singbox" = true ] && systemctl is-active --quiet singbox.service; then + read -r domain port < <(get_singbox_domain_and_port) + if [ -n "$domain" ] && [ -n "$port" ]; then + echo -e "\nSingbox Sublink:\nhttps://$domain:$port/sub/singbox/$username/$ip_version#$username\n" fi fi else From afacc27742398bfb8f33e5aa1966cd5de4176e24 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:13:38 +0330 Subject: [PATCH 25/38] chown user hysteria --- core/scripts/singbox/singbox_shell.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/scripts/singbox/singbox_shell.sh b/core/scripts/singbox/singbox_shell.sh index 708d6f5..9db7a44 100644 --- a/core/scripts/singbox/singbox_shell.sh +++ b/core/scripts/singbox/singbox_shell.sh @@ -76,7 +76,8 @@ start_service() { update_env_file "$domain" "$port" create_service_file - + chown -R hysteria:hysteria "/etc/letsencrypt/live/$domain" + chown -R hysteria:hysteria /etc/hysteria/core/scripts/singbox systemctl daemon-reload systemctl enable singbox.service > /dev/null 2>&1 systemctl start singbox.service > /dev/null 2>&1 From a99ddc3b31c2066b9820aa19ddae543c12f672bc Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:18:03 +0330 Subject: [PATCH 26/38] Fix QrCode --- core/scripts/hysteria2/show_user_uri.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/scripts/hysteria2/show_user_uri.sh b/core/scripts/hysteria2/show_user_uri.sh index 4549733..c3269ea 100644 --- a/core/scripts/hysteria2/show_user_uri.sh +++ b/core/scripts/hysteria2/show_user_uri.sh @@ -77,6 +77,24 @@ show_uri() { fi fi + if [ "$generate_qrcode" = true ]; then + cols=$(tput cols) + if [ -n "$URI" ]; then + qr1=$(echo -n "$URI" | qrencode -t UTF8 -s 3 -m 2) + echo -e "\nIPv4 QR Code:\n" + echo "$qr1" | while IFS= read -r line; do + printf "%*s\n" $(( (${#line} + cols) / 2)) "$line" + done + fi + if [ -n "$URI6" ]; then + qr2=$(echo -n "$URI6" | qrencode -t UTF8 -s 3 -m 2) + echo -e "\nIPv6 QR Code:\n" + echo "$qr2" | while IFS= read -r line; do + printf "%*s\n" $(( (${#line} + cols) / 2)) "$line" + done + fi + fi + if [ "$generate_singbox" = true ] && systemctl is-active --quiet singbox.service; then read -r domain port < <(get_singbox_domain_and_port) if [ -n "$domain" ] && [ -n "$port" ]; then From ea7b42c0377d1d99602654f97663468955f2e36b Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:33:28 +0330 Subject: [PATCH 27/38] Update variables --- core/scripts/singbox/singbox_shell.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/scripts/singbox/singbox_shell.sh b/core/scripts/singbox/singbox_shell.sh index 9db7a44..10c2edb 100644 --- a/core/scripts/singbox/singbox_shell.sh +++ b/core/scripts/singbox/singbox_shell.sh @@ -30,10 +30,10 @@ update_env_file() { local cert_dir="/etc/letsencrypt/live/$domain" cat < /etc/hysteria/core/scripts/singbox/.env -hysteria_DOMAIN=$domain -hysteria_PORT=$port -hysteria_CERTFILE=$cert_dir/fullchain.pem -hysteria_KEYFILE=$cert_dir/privkey.pem +HYSTERIA_DOMAIN=$domain +HYSTERIA_PORT=$port +HYSTERIA_CERTFILE=$cert_dir/fullchain.pem +HYSTERIA_KEYFILE=$cert_dir/privkey.pem EOL } From 69159ea05089c10ea7b8ac1d89d31dae73d22248 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:33:54 +0330 Subject: [PATCH 28/38] Update variables --- core/scripts/singbox/singbox.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/scripts/singbox/singbox.py b/core/scripts/singbox/singbox.py index 3e21bb6..3383ee0 100644 --- a/core/scripts/singbox/singbox.py +++ b/core/scripts/singbox/singbox.py @@ -13,10 +13,10 @@ from dotenv import load_dotenv load_dotenv() # Environment variables -DOMAIN = os.getenv('hysteria_DOMAIN') -CERTFILE = os.getenv('hysteria_CERTFILE') -KEYFILE = os.getenv('hysteria_KEYFILE') -PORT = int(os.getenv('hysteria_PORT', '3324')) +DOMAIN = os.getenv('HYSTERIA_DOMAIN') +CERTFILE = os.getenv('HYSTERIA_CERTFILE') +KEYFILE = os.getenv('HYSTERIA_KEYFILE') +PORT = int(os.getenv('HYSTERIA_PORT', '3324')) RATE_LIMIT = 100 RATE_LIMIT_WINDOW = 60 From b5f3b0b459f28188208c45521683579e704c3ac7 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:35:00 +0330 Subject: [PATCH 29/38] Update variables --- core/scripts/hysteria2/show_user_uri.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/scripts/hysteria2/show_user_uri.sh b/core/scripts/hysteria2/show_user_uri.sh index c3269ea..f2a8b35 100644 --- a/core/scripts/hysteria2/show_user_uri.sh +++ b/core/scripts/hysteria2/show_user_uri.sh @@ -5,8 +5,8 @@ source /etc/hysteria/core/scripts/path.sh get_singbox_domain_and_port() { if [ -f "$SINGBOX_ENV" ]; then local domain port - domain=$(grep -E '^hysteria_DOMAIN=' "$SINGBOX_ENV" | cut -d'=' -f2) - port=$(grep -E '^hysteria_PORT=' "$SINGBOX_ENV" | cut -d'=' -f2) + domain=$(grep -E '^HYSTERIA_DOMAIN=' "$SINGBOX_ENV" | cut -d'=' -f2) + port=$(grep -E '^HYSTERIA_PORT=' "$SINGBOX_ENV" | cut -d'=' -f2) echo "$domain" "$port" else echo "" From 39850fe8d3e12506af67f344294bef999145a4e9 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:43:09 +0330 Subject: [PATCH 30/38] Clone Dev --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index e4cd4c0..78175fd 100644 --- a/install.sh +++ b/install.sh @@ -13,7 +13,7 @@ if ! command -v jq &> /dev/null || ! command -v git &> /dev/null || ! command -v apt update && apt upgrade -y && apt install jq qrencode curl pwgen uuid-runtime python3 python3-pip git -y fi -git clone https://github.com/ReturnFI/Hysteria2 /etc/hysteria +git clone -b Dev https://github.com/ReturnFI/Hysteria2 /etc/hysteria # Add alias 'hys2' for Hysteria2 if ! grep -q "alias hys2='/etc/hysteria/menu.sh'" ~/.bashrc; then From 2d4d5f8aebc979485ea2175d7d916810003d7133 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 23:32:37 +0330 Subject: [PATCH 31/38] Fix traffic data --- core/scripts/hysteria2/get_user.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/scripts/hysteria2/get_user.sh b/core/scripts/hysteria2/get_user.sh index 5694c40..6d6c5bd 100644 --- a/core/scripts/hysteria2/get_user.sh +++ b/core/scripts/hysteria2/get_user.sh @@ -25,14 +25,14 @@ if [ -z "$USERNAME" ]; then fi if [ ! -f "$USERS_FILE" ]; then - echo "users.json file not found!" + echo "users.json file not found at $USERS_FILE!" exit 1 fi -USER_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' $USERS_FILE) +USER_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' "$USERS_FILE") if [ -z "$USER_INFO" ]; then - echo "User '$USERNAME' not found." + echo "User '$USERNAME' not found in $USERS_FILE." exit 1 fi @@ -40,14 +40,14 @@ echo "$USER_INFO" | jq . if [ "$SHOW_TRAFFIC" = true ]; then if [ ! -f "$TRAFFIC_FILE" ]; then - echo "traffic_data.json file not found!" - exit 1 + echo "No traffic data file found at $TRAFFIC_FILE. User might not have connected yet." + exit 0 fi - TRAFFIC_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' $TRAFFIC_FILE) + TRAFFIC_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' "$TRAFFIC_FILE") if [ -z "$TRAFFIC_INFO" ]; then - echo "No traffic data found for user '$USERNAME'." + echo "No traffic data found for user '$USERNAME' in $TRAFFIC_FILE. User might not have connected yet." else echo "$TRAFFIC_INFO" | jq . fi From 7ebaaaeb9a8ebcbaab35605e29ede193ef2f0bc5 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 23:34:14 +0330 Subject: [PATCH 32/38] Fix traffic data --- core/scripts/telegrambot/tbot.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/core/scripts/telegrambot/tbot.py b/core/scripts/telegrambot/tbot.py index 83e9272..3dc8028 100644 --- a/core/scripts/telegrambot/tbot.py +++ b/core/scripts/telegrambot/tbot.py @@ -90,7 +90,8 @@ def process_show_user(message): username = message.text.strip() command = f"python3 {CLI_PATH} get-user -u {username}" user_result = run_cli_command(command) - user_json_match = re.search(r'(\{.*?\})\n(\{.*?\})', user_result, re.DOTALL) + + user_json_match = re.search(r'(\{.*?\})\n?(\{.*?\})?', user_result, re.DOTALL) if not user_json_match: bot.reply_to(message, "Failed to parse user details. The command output format may be incorrect.") @@ -101,13 +102,17 @@ def process_show_user(message): try: user_details = json.loads(user_json) - traffic_data = json.loads(traffic_data_section) - traffic_message = ( - f"**Traffic Data:**\n" - f"Upload: {traffic_data.get('upload_bytes', 0) / (1024 ** 2):.2f} MB\n" - f"Download: {traffic_data.get('download_bytes', 0) / (1024 ** 2):.2f} MB\n" - f"Status: {traffic_data.get('status', 'Unknown')}" - ) + + if traffic_data_section: + traffic_data = json.loads(traffic_data_section) + traffic_message = ( + f"**Traffic Data:**\n" + f"Upload: {traffic_data.get('upload_bytes', 0) / (1024 ** 2):.2f} MB\n" + f"Download: {traffic_data.get('download_bytes', 0) / (1024 ** 2):.2f} MB\n" + f"Status: {traffic_data.get('status', 'Unknown')}" + ) + else: + traffic_message = "**Traffic Data:**\nNo traffic data available. The user might not have connected yet." except json.JSONDecodeError: bot.reply_to(message, "Failed to parse JSON data. The command output may be malformed.") return From f386da6f93d51cfd7ad81fcf1c132b384e0cf1c7 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Mon, 12 Aug 2024 23:48:25 +0330 Subject: [PATCH 33/38] Fix Add User --- core/scripts/telegrambot/tbot.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/scripts/telegrambot/tbot.py b/core/scripts/telegrambot/tbot.py index 3dc8028..2c06ebb 100644 --- a/core/scripts/telegrambot/tbot.py +++ b/core/scripts/telegrambot/tbot.py @@ -52,14 +52,19 @@ def process_add_user_step1(message): command = f"python3 {CLI_PATH} list-users" result = run_cli_command(command) + try: users = json.loads(result) + if username in users: bot.reply_to(message, f"Username '{username}' already exists. Please choose a different username.") return except json.JSONDecodeError: - bot.reply_to(message, "Error retrieving user list. Please try again later.") - return + if "No such file or directory" in result or result.strip() == "": + bot.reply_to(message, "User list file does not exist. Adding the first user.") + else: + bot.reply_to(message, "Error retrieving user list. Please try again later.") + return msg = bot.reply_to(message, "Enter traffic limit (GB):") bot.register_next_step_handler(msg, process_add_user_step2, username) From dd42844c9359e4d73b37b8d080fca02a64d826f7 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:11:39 +0330 Subject: [PATCH 34/38] Hysteria2_CLI_Usage.md --- Hysteria2_CLI_Usage.md | 203 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 Hysteria2_CLI_Usage.md diff --git a/Hysteria2_CLI_Usage.md b/Hysteria2_CLI_Usage.md new file mode 100644 index 0000000..25c5e4f --- /dev/null +++ b/Hysteria2_CLI_Usage.md @@ -0,0 +1,203 @@ +# Hysteria2 CLI Tool + +## Overview + +The Hysteria2 CLI Tool is a command-line utility for managing various aspects of Hysteria2 and related services on your system. It allows you to install, configure, and manage Hysteria2, as well as other services like Telegram bot, Singbox SubLink, TCP Brutal, and WARP. + +## Requirements + +- Python 3.6 or higher +- `pwgen` for password generation +- Required scripts located in `/etc/hysteria/core/scripts` +- cli.py located in `/etc/hysteria/core` + +## Commands + +### Hysteria2 Management + +- **Install Hysteria2** + + ```sh + cli.py install-hysteria2 --port + ``` + + *Options:* + - `--port, -p`: New port for Hysteria2 (required) + +- **Uninstall Hysteria2** + + ```sh + cli.py uninstall-hysteria2 + ``` + +- **Update Hysteria2** + + ```sh + cli.py update-hysteria2 + ``` + +- **Restart Hysteria2** + + ```sh + cli.py restart-hysteria2 + ``` + +- **Change Hysteria2 Port** + + ```sh + cli.py change-hysteria2-port --port + ``` + + *Options:* + - `--port, -p`: New port for Hysteria2 (required) + +### User Management + +- **Get User Information** + + ```sh + cli.py get-user --username [--no-traffic] + ``` + + *Options:* + - `--username, -u`: Username for the user (required) + - `--no-traffic, -t`: Do not display traffic information (optional) + +- **Add User** + + ```sh + cli.py add-user --username --traffic-limit --expiration-days [--password ] [--creation-date ] + ``` + + *Options:* + - `--username, -u`: Username for the new user (required) + - `--traffic-limit, -t`: Traffic limit in GB (required) + - `--expiration-days, -e`: Expiration days (required) + - `--password, -p`: Password (optional; will be generated if not provided) + - `--creation-date, -c`: Creation date (optional; defaults to current date) + +- **Edit User** + + ```sh + cli.py edit-user --username [--new-username ] [--new-traffic-limit ] [--new-expiration-days ] [--renew-password] [--renew-creation-date] [--blocked] + ``` + + *Options:* + - `--username, -u`: Username to edit (required) + - `--new-username, -nu`: New username (optional) + - `--new-traffic-limit, -nt`: New traffic limit in GB (optional) + - `--new-expiration-days, -ne`: New expiration days (optional) + - `--renew-password, -rp`: Renew password (optional) + - `--renew-creation-date, -rc`: Renew creation date (optional) + - `--blocked, -b`: Block the user (optional) + +- **Reset User** + + ```sh + cli.py reset-user --username + ``` + + *Options:* + - `--username, -u`: Username to reset (required) + +- **Remove User** + + ```sh + cli.py remove-user --username + ``` + + *Options:* + - `--username, -u`: Username to remove (required) + +- **Show User URI** + + ```sh + cli.py show-user-uri --username [--qrcode] [--ipv <4|6>] [--all] [--singbox] + ``` + + *Options:* + - `--username, -u`: Username for the user (required) + - `--qrcode, -qr`: Generate QR code for the URI (optional) + - `--ipv, -ip`: IP version (4 or 6; default is 4) + - `--all, -a`: Show both IPv4 and IPv6 URIs (optional) + - `--singbox, -s`: Generate Singbox sublink if Singbox service is active (optional) + +- **List Users** + + ```sh + cli.py list-users + ``` + +- **Server Info** + + ```sh + cli.py server-info + ``` + +### Advanced Commands + +- **Install TCP Brutal** + + ```sh + cli.py install-tcp-brutal + ``` + +- **Install WARP** + + ```sh + cli.py install-warp + ``` + +- **Uninstall WARP** + + ```sh + cli.py uninstall-warp + ``` + +- **Configure WARP** + + ```sh + cli.py configure-warp [--all] [--popular-sites] [--domestic-sites] [--block-adult-sites] + ``` + + *Options:* + - `--all, -a`: Use WARP for all connections (optional) + - `--popular-sites, -p`: Use WARP for popular sites (optional) + - `--domestic-sites, -d`: Use WARP for domestic sites (optional) + - `--block-adult-sites, -x`: Block adult content (optional) + +- **Telegram Bot Management** + + ```sh + cli.py telegram --action [--token ] [--adminid ] + ``` + + *Options:* + - `--action, -a`: Action to perform: `start` or `stop` (required) + - `--token, -t`: Token for the bot (required for `start`) + - `--adminid, -aid`: Telegram admin IDs (required for `start`) + +- **Singbox SubLink** + + ```sh + cli.py singbox --action [--domain ] [--port ] + ``` + + *Options:* + - `--action, -a`: Action to perform: `start` or `stop` (required) + - `--domain, -d`: Domain name for SSL (required for `start`) + - `--port, -p`: Port number for Singbox service (required for `start`) + +## Debugging + +To enable debugging, set the `DEBUG` variable to `True` in the script: + +```python +DEBUG = True +``` + +This will print the commands being executed. + +## Contributing + +Feel free to contribute by creating issues or submitting pull requests on the [GitHub repository](https://github.com/ReturnFI/Hysteria2). From e87bcdb13b9360cb068672f67ab58c26316b27bf Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:24:05 +0330 Subject: [PATCH 35/38] Update README.md --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ceb6607..6ac7e1a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ # Hysteria2 Management Shell Script -This shell script provides a menu-driven interface to manage Hysteria2 server operations. It includes options to install, configure, update, and uninstall Hysteria2, as well as manage users, ports, traffic status, and integrate with other tools like TCP Brutal and WARP. +This bash script provides a comprehensive menu-driven interface to manage the Hysteria2 server, user accounts, and various services. It supports installation, user management, traffic monitoring, and integration with additional tools like WARP, Singbox SubLink, and a Telegram bot. + ### Install command : ```shell @@ -28,25 +29,50 @@ bash <(curl https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/upgrade.sh

-## Features +## Features : -### Hysteria2: -- Install and Configure: Installs and configures Hysteria2 server. -- Add User: Creates a new user for Hysteria2 access. -- Show URI: Displays the connection URI and QR code for existing users. -- Check Traffic Status: Monitors real-time traffic information for each user. -- Remove User: Deletes a user from the Hysteria2 configuration. -- Change Port: Modifies the listening port for the Hysteria2 server. -- Update Core: Updates Hysteria2 to the latest available version. -- Uninstall Hysteria2: Removes Hysteria2 server and its configuration. +- **Hysteria2 Installation & Configuration:** + - Install and configure Hysteria2 on your server. + - Manage user accounts (add, edit, reset, remove, list). + - Monitor traffic and display user URIs. -### Advance: (Optional features) +- **Advanced Options:** + - Install and manage additional services like WARP and TCP Brutal. + - Start/Stop Singbox SubLink and Telegram bot services. + - Change the port number for Hysteria2. + - Update or uninstall Hysteria2. -- Install TCP Brutal: The script can optionally install TCP Brutal, a tool designed to improve performance on congested networks. -- Install WARP: Integrate WARP from Cloudflare to add an extra layer of encryption to your Hysteria2 connections, further protecting your online activity. -- Configure WARP: Manages WARP integration with Hysteria2 for traffic routing. +- **Interactive Menus:** + - User-friendly menu-driven interface for easier navigation and management. + - Validations for user inputs and system checks to prevent misconfigurations. -## Prerequisites + +## Main Menu : + +- **Hysteria2 Menu:** + - **Install and Configure Hysteria2:** Set up Hysteria2 with your desired configuration. + - **Add User:** Add a new user with traffic limits and expiration days. + - **Edit User:** Modify user details like username, traffic limit, expiration days, password, etc. + - **Reset User:** Reset user statistics. + - **Remove User:** Remove a user from the system. + - **Get User:** Retrieve detailed information of a specific user. + - **List Users:** Display a list of all users. + - **Check Traffic Status:** View the current traffic status. + - **Show User URI:** Generate and display the URI for a user. + +- **Advance Menu:** + - **Install TCP Brutal:** Install the TCP Brutal service. + - **Install WARP:** Install Cloudflare's WARP service. + - **Configure WARP:** Configure WARP for different traffic routes. + - **Uninstall WARP:** Remove WARP from the system. + - **Telegram Bot:** Start or stop the Telegram bot service. + - **Singbox SubLink:** Start or stop the Singbox service. + - **Change Port Hysteria2:** Change the port on which Hysteria2 listens. + - **Update Core Hysteria2:** Update Hysteria2 to the latest version. + - **Uninstall Hysteria2:** Remove Hysteria2 and its configuration. + + +## Prerequisites : Ensure the following packages are installed: - Ubuntu-based Linux distribution (tested on Ubuntu) @@ -57,8 +83,16 @@ Ensure the following packages are installed: - uuid-runtime If any of these are missing, the script will attempt to install them automatically. - -## Disclaimer: + +## Contributing : + +Contributions are welcome! + +Feel free to contribute by creating issues or submitting pull requests + +Please fork the repository and submit a pull request with your improvements. + +## Disclaimer : This script is provided for educational purposes only. The developer are not responsible for any misuse or consequences arising from its use. Please ensure you understand the implications of using Hysteria2 and related tools before deployment in a production environment. From 5bb10ba46bf5a7810b548f2fe4b6a0e370c4beb6 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:35:31 +0330 Subject: [PATCH 36/38] Update README-fa.md --- README-fa.md | 85 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/README-fa.md b/README-fa.md index 344fb8f..f0865b1 100644 --- a/README-fa.md +++ b/README-fa.md @@ -1,17 +1,20 @@ -### اسکریپت مدیریتی Hysteria2 +

-این اسکریپت شل یک رابط کاربری مبتنی بر منو فراهم می‌کند برای مدیریت Hysteria2 میباشد. +# اسکریپت مدیریت Hysteria2 -این اسکریپت شامل گزینه‌های نصب، پیکربندی، به روزرسانی و حذف Hysteria2 است، همچنین مدیریت کاربران، پورت‌ها، وضعیت ترافیک و ادغام با ابزارهای دیگر مانند TCP Brutal و WARP. +این اسکریپت یک رابط کاربری جامع مبتنی بر منو را برای مدیریت سرور Hysteria2، حساب‌های کاربری و خدمات مختلف فراهم می‌کند. + +این اسکریپت از نصب، مدیریت کاربران، نظارت بر ترافیک و ادغام با ابزارهای اضافی مانند وارپ ، ساب لینک سینگ باکس و ربات تلگرام پشتیبانی می‌کند. ### دستور نصب: - ```shell -bash <(curl https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/menu.sh) +bash <(curl https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/install.sh) ``` -بعد از نصب کافیه از دستور `hys2` برای اجرای اسکریپت Hysteria2 استفاده کنید و نیازی به اجرا دوباره دستور نصب نیست. +پس از نصب، فقط از دستور `hys2` برای اجرای اسکریپت Hysteria2 استفاده کنید. -### دستور آپدیت: +نیازی به اجرای مجدد دستور نصب نیست. + +### دستور ارتقاء: ```shell bash <(curl https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/upgrade.sh) ``` @@ -21,33 +24,63 @@ bash <(curl https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/upgrade.sh

-## ویژگی‌ها Hysteria2: +## ویژگی‌ها: -- نصب و پیکربندی: نصب و پیکربندی سرور Hysteria2. -- افزودن کاربر: ایجاد کاربر جدید برای دسترسی به Hysteria2. -- نمایش URI: نمایش URI اتصال و کد QR برای کاربران موجود. -- بررسی وضعیت ترافیک: نظارت بر اطلاعات ترافیک به صورت زمان واقعی برای هر کاربر. -- حذف کاربر: حذف یک کاربر از پیکربندی Hysteria2. -- تغییر پورت: تغییر پورت‌ گوش‌ دادن برای سرور Hysteria2. -- به روزرسانی هسته: به روزرسانی Hysteria2 به آخرین نسخه موجود. -- حذف نصب Hysteria2: حذف سرور Hysteria2 و پیکربندی آن. +**نصب و پیکربندی Hysteria2:** + + - نصب و پیکربندی Hysteria2 روی سرور خود. + - مدیریت حساب‌های کاربری (افزودن، ویرایش، بازنشانی، حذف، لیست). + - نظارت بر ترافیک و نمایش URIهای کاربر. + +**گزینه‌های پیشرفته:** + + - نصب و مدیریت خدمات اضافی مانند WARP و TCP Brutal. + - شروع/توقف خدمات Singbox SubLink و ربات تلگرام. + - تغییر شماره پورت برای Hysteria2. + - به‌روزرسانی یا حذف نصب Hysteria2. + +**منوهای تعاملی:** + + - رابط کاربری مبتنی بر منو برای راحتی در جابجایی و مدیریت. + - اعتبارسنجی برای ورودی‌های کاربر و بررسی‌های سیستمی برای جلوگیری از پیکربندی‌های نادرست. + +## منوی اصلی: -### پیشرفته: (ویژگی‌های اختیاری) -- نصب TCP Brutal: این اسکریپت می‌تواند به صورت اختیاری TCP Brutal را نصب کند، یک ابزار طراحی شده برای بهبود عملکرد در شبکه‌های شلوغ. -- نصب WARP: ادغام WARP از Cloudflare برای افزودن یک لایه اضافی از رمزنگاری به اتصالات Hysteria2 شما، که باعث حفاظت بیشتر فعالیت‌های آنلاین شما می‌شود. -- پیکربندی WARP: مدیریت ادغام WARP با Hysteria2 برای مسیریابی ترافیک. +- **نصب و پیکربندی Hysteria2:** پیکربندی Hysteria2 با تنظیمات مورد نظر. +- **افزودن کاربر:** افزودن کاربر جدید با محدودیت‌های ترافیکی و روزهای انقضا. +- **ویرایش کاربر:** تغییر جزئیات کاربر مانند نام کاربری، محدودیت ترافیک، روزهای انقضا، رمز عبور و غیره. +- **بازنشانی کاربر:** بازنشانی آمار کاربر. +- **حذف کاربر:** حذف کاربر از سیستم. +- **دریافت کاربر:** بازیابی اطلاعات دقیق کاربر خاص. +- **لیست کاربران:** نمایش لیستی از تمام کاربران. +- **بررسی وضعیت ترافیک:** مشاهده وضعیت فعلی ترافیک. +- **نمایش URI کاربر:** تولید و نمایش URI برای یک کاربر. -## پیش‌نیازها -اطمینان حاصل کنید که بسته‌های زیر نصب شده باشند: +## منوی پیشرفته: -- توزیع مبتنی بر Linux مانند Ubuntu (بر روی Ubuntu تست شده است) + - **نصب TCP Brutal:** نصب سرویس TCP Brutal. + - **نصب WARP:** نصب سرویس WARP از Cloudflare. + - **پیکربندی WARP:** پیکربندی WARP برای مسیرهای ترافیکی مختلف. + - **حذف WARP:** حذف WARP از سیستم. + - **ربات تلگرام:** شروع یا توقف سرویس ربات تلگرام. + - **ساب لینک سینگباکس:** شروع یا توقف سرویس Singbox. + - **تغییر پورت Hysteria2:** تغییر پورتی که Hysteria2 به آن گوش می‌دهد. + - **به‌روزرسانی Hysteria2:** به‌روزرسانی Hysteria2 به نسخه جدیدترین. + - **حذف نصب Hysteria2:** حذف Hysteria2 و پیکربندی‌های آن. + +## پیش‌نیازها: +اطمینان حاصل کنید که بسته‌های زیر نصب شده‌اند: + +- توزیع لینوکس مبتنی بر اوبونتو (تست شده بر روی اوبونتو) - jq - qrencode - curl - pwgen - uuid-runtime -در صورت عدم وجود هر کدام از این بسته‌ها، اسکریپت به صورت خودکار سعی می‌کند آن‌ها را نصب کند. +اگر هر یک از این موارد وجود نداشته باشد، اسکریپت سعی خواهد کرد آنها را به طور خودکار نصب کند. -## سلب مسئولیت: -این اسکریپت تنها برای اهداف آموزشی فراهم شده است. توسعه‌دهندگان هیچگونه مسئولیتی در قبال هرگونه سوءاستفاده یا عواقب ناشی از استفاده از آن ندارند. لطفاً پیش از استقرار در محیط تولید، پیامدهای استفاده از Hysteria2 و ابزارهای مرتبط را درک کنید. + +## ملاحظه: + +این اسکریپت تنها برای مقاصد آموزشی ارائه شده است. توسعه‌دهندگان هیچ مسئولیتی در قبال سوءاستفاده یا پیامدهای ناشی از استفاده از آن ندارند. لطفاً قبل از استقرار در محیط تولید، مطمئن شوید که پیامدهای استفاده از Hysteria2 و ابزارهای مرتبط را درک کرده‌اید. From 3013c90a151fd241876b02a79b293ea857ba60ab Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:38:33 +0330 Subject: [PATCH 37/38] Update Hysteria2_CLI_Usage.md --- Hysteria2_CLI_Usage.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Hysteria2_CLI_Usage.md b/Hysteria2_CLI_Usage.md index 25c5e4f..06cb1a8 100644 --- a/Hysteria2_CLI_Usage.md +++ b/Hysteria2_CLI_Usage.md @@ -4,14 +4,7 @@ The Hysteria2 CLI Tool is a command-line utility for managing various aspects of Hysteria2 and related services on your system. It allows you to install, configure, and manage Hysteria2, as well as other services like Telegram bot, Singbox SubLink, TCP Brutal, and WARP. -## Requirements - -- Python 3.6 or higher -- `pwgen` for password generation -- Required scripts located in `/etc/hysteria/core/scripts` -- cli.py located in `/etc/hysteria/core` - -## Commands +## Commands: ### Hysteria2 Management From 386b64d8bceedbc4d8a9ba1dfed288d2cb508f65 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:22:19 +0330 Subject: [PATCH 38/38] Update VERSION --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 845639e..9faa1b7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.4 +0.1.5