From 133684f4fe19fe0d9bddc075d286ae6fd0c0f40e Mon Sep 17 00:00:00 2001
From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com>
Date: Wed, 6 Aug 2025 15:16:27 +0330
Subject: [PATCH] fix: Resolve IP/domain validation
---
core/scripts/hysteria2/node.py | 41 +++-
.../routers/api/v1/schema/config/ip.py | 46 ++--
core/scripts/webpanel/templates/settings.html | 214 ++++++++++++++----
3 files changed, 229 insertions(+), 72 deletions(-)
diff --git a/core/scripts/hysteria2/node.py b/core/scripts/hysteria2/node.py
index 2c7921b..1de692f 100644
--- a/core/scripts/hysteria2/node.py
+++ b/core/scripts/hysteria2/node.py
@@ -4,14 +4,33 @@ import sys
import json
import argparse
from pathlib import Path
+import re
+from ipaddress import ip_address
core_scripts_dir = Path(__file__).resolve().parents[1]
if str(core_scripts_dir) not in sys.path:
sys.path.append(str(core_scripts_dir))
+try:
from paths import NODES_JSON_PATH
+except ImportError:
+ NODES_JSON_PATH = Path("/etc/hysteria/nodes.json")
+def is_valid_ip_or_domain(value: str) -> bool:
+ """Check if the value is a valid IP address or domain name."""
+ if not value or not value.strip():
+ return False
+ value = value.strip()
+ try:
+ ip_address(value)
+ return True
+ except ValueError:
+ domain_regex = re.compile(
+ r'^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$',
+ re.IGNORECASE
+ )
+ return re.match(domain_regex, value) is not None
def read_nodes():
if not NODES_JSON_PATH.exists():
@@ -22,10 +41,8 @@ def read_nodes():
if not content:
return []
return json.loads(content)
- except json.JSONDecodeError:
- sys.exit(f"Error: Could not decode JSON from {NODES_JSON_PATH}")
- except (IOError, OSError) as e:
- sys.exit(f"Error reading from {NODES_JSON_PATH}: {e}")
+ except (json.JSONDecodeError, IOError, OSError) as e:
+ sys.exit(f"Error reading or parsing {NODES_JSON_PATH}: {e}")
def write_nodes(nodes):
try:
@@ -36,17 +53,21 @@ def write_nodes(nodes):
sys.exit(f"Error writing to {NODES_JSON_PATH}: {e}")
def add_node(name: str, ip: str):
+ if not is_valid_ip_or_domain(ip):
+ print(f"Error: '{ip}' is not a valid IP address or domain name.", file=sys.stderr)
+ sys.exit(1)
+
nodes = read_nodes()
if any(node['name'] == name for node in nodes):
print(f"Error: A node with the name '{name}' already exists.", file=sys.stderr)
sys.exit(1)
if any(node['ip'] == ip for node in nodes):
- print(f"Error: A node with the IP '{ip}' already exists.", file=sys.stderr)
+ print(f"Error: A node with the IP/domain '{ip}' already exists.", file=sys.stderr)
sys.exit(1)
nodes.append({"name": name, "ip": ip})
write_nodes(nodes)
- print(f"Successfully added node '{name}' with IP '{ip}'.")
+ print(f"Successfully added node '{name}' with IP/domain '{ip}'.")
def delete_node(name: str):
nodes = read_nodes()
@@ -66,10 +87,10 @@ def list_nodes():
print("No nodes configured.")
return
- print(f"{'Name':<20} {'IP Address'}")
- print(f"{'-'*20} {'-'*15}")
+ print(f"{'Name':<30} {'IP Address / Domain'}")
+ print(f"{'-'*30} {'-'*25}")
for node in sorted(nodes, key=lambda x: x['name']):
- print(f"{node['name']:<20} {node['ip']}")
+ print(f"{node['name']:<30} {node['ip']}")
def main():
parser = argparse.ArgumentParser(description="Manage external node configurations.")
@@ -77,7 +98,7 @@ def main():
add_parser = subparsers.add_parser('add', help='Add a new node.')
add_parser.add_argument('--name', type=str, required=True, help='The unique name of the node.')
- add_parser.add_argument('--ip', type=str, required=True, help='The IP address of the node.')
+ add_parser.add_argument('--ip', type=str, required=True, help='The IP address or domain of the node.')
delete_parser = subparsers.add_parser('delete', help='Delete a node by name.')
delete_parser.add_argument('--name', type=str, required=True, help='The name of the node to delete.')
diff --git a/core/scripts/webpanel/routers/api/v1/schema/config/ip.py b/core/scripts/webpanel/routers/api/v1/schema/config/ip.py
index 31589f9..538d2e7 100644
--- a/core/scripts/webpanel/routers/api/v1/schema/config/ip.py
+++ b/core/scripts/webpanel/routers/api/v1/schema/config/ip.py
@@ -1,24 +1,32 @@
from pydantic import BaseModel, field_validator, ValidationInfo
from ipaddress import IPv4Address, IPv6Address, ip_address
-import socket
+import re
+
+def validate_ip_or_domain(v: str) -> str | None:
+ if v is None or v.strip() in ['', 'None']:
+ return None
+
+ v_stripped = v.strip()
+
+ try:
+ ip_address(v_stripped)
+ return v_stripped
+ except ValueError:
+ domain_regex = re.compile(
+ r'^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$',
+ re.IGNORECASE
+ )
+ if domain_regex.match(v_stripped):
+ return v_stripped
+ raise ValueError(f"'{v_stripped}' is not a valid IP address or domain name.")
class StatusResponse(BaseModel):
ipv4: str | None = None
ipv6: str | None = None
@field_validator('ipv4', 'ipv6', mode='before')
- def check_ip_or_domain(cls, v: str, info: ValidationInfo):
- if v is None:
- return v
- try:
- ip_address(v)
- return v
- except ValueError:
- try:
- socket.getaddrinfo(v, None)
- return v
- except socket.gaierror:
- raise ValueError(f"'{v}' is not a valid IPv4 or IPv6 address or domain name")
+ def check_local_server_ip(cls, v: str | None):
+ return validate_ip_or_domain(v)
class EditInputBody(StatusResponse):
pass
@@ -28,14 +36,10 @@ class Node(BaseModel):
ip: str
@field_validator('ip', mode='before')
- def check_ip(cls, v: str, info: ValidationInfo):
- if v is None:
- raise ValueError("IP cannot be None")
- try:
- ip_address(v)
- return v
- except ValueError:
- raise ValueError(f"'{v}' is not a valid IPv4 or IPv6 address")
+ def check_node_ip(cls, v: str | None):
+ if not v or not v.strip():
+ raise ValueError("IP or Domain field cannot be empty.")
+ return validate_ip_or_domain(v)
class AddNodeBody(Node):
pass
diff --git a/core/scripts/webpanel/templates/settings.html b/core/scripts/webpanel/templates/settings.html
index 198a0e0..b9f78eb 100644
--- a/core/scripts/webpanel/templates/settings.html
+++ b/core/scripts/webpanel/templates/settings.html
@@ -49,7 +49,7 @@
- Change IP
+ IP Management
-
+
-
@@ -436,6 +476,7 @@
initUI();
fetchDecoyStatus();
fetchObfsStatus();
+ fetchNodes();
function isValidPath(path) {
if (!path) return false;
@@ -458,20 +499,17 @@
return /^[a-zA-Z0-9]+$/.test(subpath);
}
-
function isValidIPorDomain(input) {
- if (!input) return true;
-
+ if (input === null || typeof input === 'undefined') return false;
+ input = input.trim();
+ if (input === '') return false;
+
const ipV4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipV6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}([0-9a-fA-F]{1,4}|:)|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
const domainRegex = /^(?!-)(?:[a-zA-Z\d-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+$)[a-zA-Z\d]{1,63}$/;
const lowerInput = input.toLowerCase();
- if (ipV4Regex.test(input)) return true;
- if (ipV6Regex.test(input)) return true;
- if (domainRegex.test(lowerInput) && !lowerInput.startsWith("http://") && !lowerInput.startsWith("https://")) return true;
-
- return false;
+ return ipV4Regex.test(input) || ipV6Regex.test(input) || domainRegex.test(lowerInput);
}
function isValidPositiveNumber(value) {
@@ -518,12 +556,16 @@
}
}
});
- console.log("Success Response:", response);
},
error: function (xhr, status, error) {
- let errorMessage = "Something went wrong.";
+ let errorMessage = "An unexpected error occurred.";
if (xhr.responseJSON && xhr.responseJSON.detail) {
- errorMessage = xhr.responseJSON.detail;
+ const detail = xhr.responseJSON.detail;
+ if (Array.isArray(detail)) {
+ errorMessage = detail.map(err => `Error in '${err.loc[1]}': ${err.msg}`).join('\n');
+ } else if (typeof detail === 'string') {
+ errorMessage = detail;
+ }
}
Swal.fire("Error!", errorMessage, "error");
console.error("AJAX Error:", status, error, xhr.responseText);
@@ -552,6 +594,10 @@
fieldValid = isValidSubPath(input.val());
} else if (id === 'ipv4' || id === 'ipv6') {
fieldValid = (input.val().trim() === '') ? true : isValidIPorDomain(input.val());
+ } else if (id === 'node_ip') {
+ fieldValid = isValidIPorDomain(input.val());
+ } else if (id === 'node_name') {
+ fieldValid = input.val().trim() !== "";
} else if (id === 'block_duration' || id === 'max_ips') {
fieldValid = isValidPositiveNumber(input.val());
} else if (id === 'decoy_path') {
@@ -572,7 +618,6 @@
return isValid;
}
-
function initUI() {
$.ajax({
url: "{{ url_for('server_services_status_api') }}",
@@ -620,6 +665,83 @@
}
});
}
+
+ function fetchNodes() {
+ $.ajax({
+ url: "{{ url_for('get_all_nodes') }}",
+ type: "GET",
+ success: function (nodes) {
+ renderNodes(nodes);
+ },
+ error: function(xhr) {
+ Swal.fire("Error!", "Failed to fetch external nodes list.", "error");
+ console.error("Error fetching nodes:", xhr.responseText);
+ }
+ });
+ }
+
+ function renderNodes(nodes) {
+ const tableBody = $("#nodes_table tbody");
+ tableBody.empty();
+
+ if (nodes && nodes.length > 0) {
+ $("#nodes_table").show();
+ $("#no_nodes_message").hide();
+ nodes.forEach(node => {
+ const row = `
+ | ${node.name} |
+ ${node.ip} |
+
+
+ |
+
`;
+ tableBody.append(row);
+ });
+ } else {
+ $("#nodes_table").hide();
+ $("#no_nodes_message").show();
+ }
+ }
+
+ function addNode() {
+ if (!validateForm('add_node_form')) return;
+
+ const name = $("#node_name").val().trim();
+ const ip = $("#node_ip").val().trim();
+
+ confirmAction(`add the node '${name}'`, function () {
+ sendRequest(
+ "{{ url_for('add_node') }}",
+ "POST",
+ { name: name, ip: ip },
+ `Node '${name}' added successfully!`,
+ "#add_node_btn",
+ false,
+ function() {
+ $("#node_name").val('');
+ $("#node_ip").val('');
+ $("#add_node_form .form-control").removeClass('is-invalid');
+ fetchNodes();
+ }
+ );
+ });
+ }
+
+ function deleteNode(nodeName) {
+ confirmAction(`delete the node '${nodeName}'`, function () {
+ sendRequest(
+ "{{ url_for('delete_node') }}",
+ "POST",
+ { name: nodeName },
+ `Node '${nodeName}' deleted successfully!`,
+ null,
+ false,
+ fetchNodes
+ );
+ });
+ }
function updateServiceUI(data) {
const servicesMap = {
@@ -633,7 +755,6 @@
let targetSelector = servicesMap[serviceKey];
let isRunning = data[serviceKey];
-
if (serviceKey === "hysteria_normal_sub") {
const $normalForm = $("#normal_sub_service_form");
const $normalFormGroups = $normalForm.find(".form-group");
@@ -771,7 +892,6 @@
});
}
-
function setupDecoy() {
if (!validateForm('decoy_form')) return;
const domain = $("#decoy_domain").val();
@@ -846,7 +966,6 @@
}
}
-
function fetchObfsStatus() {
$.ajax({
url: "{{ url_for('check_obfs') }}",
@@ -908,7 +1027,6 @@
});
}
-
function startTelegram() {
if (!validateForm('telegram_form')) return;
const apiToken = $("#telegram_api_token").val();
@@ -1197,7 +1315,6 @@
});
});
-
$("#telegram_start").on("click", startTelegram);
$("#telegram_stop").on("click", stopTelegram);
$("#normal_start").on("click", startNormal);
@@ -1215,7 +1332,11 @@
$("#decoy_stop").on("click", stopDecoy);
$("#obfs_enable_btn").on("click", enableObfs);
$("#obfs_disable_btn").on("click", disableObfs);
-
+ $("#add_node_btn").on("click", addNode);
+ $("#nodes_table").on("click", ".delete-node-btn", function() {
+ const nodeName = $(this).data("name");
+ deleteNode(nodeName);
+ });
$('#normal_domain, #sni_domain, #decoy_domain').on('input', function () {
if (isValidDomain($(this).val())) {
@@ -1247,9 +1368,20 @@
}
});
- $('#ipv4, #ipv6').on('input', function () {
- if (isValidIPorDomain($(this).val()) || $(this).val().trim() === '') {
+ $('#ipv4, #ipv6, #node_ip').on('input', function () {
+ const isLocalIpField = $(this).attr('id') === 'ipv4' || $(this).attr('id') === 'ipv6';
+ if (isLocalIpField && $(this).val().trim() === '') {
$(this).removeClass('is-invalid');
+ } else if (isValidIPorDomain($(this).val())) {
+ $(this).removeClass('is-invalid');
+ } else {
+ $(this).addClass('is-invalid');
+ }
+ });
+
+ $('#node_name').on('input', function() {
+ if ($(this).val().trim() !== "") {
+ $(this).removeClass('is-invalid');
} else {
$(this).addClass('is-invalid');
}
@@ -1284,4 +1416,4 @@
});
-{% endblock %}
+{% endblock %}
\ No newline at end of file