fix: Resolve IP/domain validation
This commit is contained in:
@ -4,14 +4,33 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from ipaddress import ip_address
|
||||||
|
|
||||||
core_scripts_dir = Path(__file__).resolve().parents[1]
|
core_scripts_dir = Path(__file__).resolve().parents[1]
|
||||||
if str(core_scripts_dir) not in sys.path:
|
if str(core_scripts_dir) not in sys.path:
|
||||||
sys.path.append(str(core_scripts_dir))
|
sys.path.append(str(core_scripts_dir))
|
||||||
|
|
||||||
|
try:
|
||||||
from paths import NODES_JSON_PATH
|
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():
|
def read_nodes():
|
||||||
if not NODES_JSON_PATH.exists():
|
if not NODES_JSON_PATH.exists():
|
||||||
@ -22,10 +41,8 @@ def read_nodes():
|
|||||||
if not content:
|
if not content:
|
||||||
return []
|
return []
|
||||||
return json.loads(content)
|
return json.loads(content)
|
||||||
except json.JSONDecodeError:
|
except (json.JSONDecodeError, IOError, OSError) as e:
|
||||||
sys.exit(f"Error: Could not decode JSON from {NODES_JSON_PATH}")
|
sys.exit(f"Error reading or parsing {NODES_JSON_PATH}: {e}")
|
||||||
except (IOError, OSError) as e:
|
|
||||||
sys.exit(f"Error reading from {NODES_JSON_PATH}: {e}")
|
|
||||||
|
|
||||||
def write_nodes(nodes):
|
def write_nodes(nodes):
|
||||||
try:
|
try:
|
||||||
@ -36,17 +53,21 @@ def write_nodes(nodes):
|
|||||||
sys.exit(f"Error writing to {NODES_JSON_PATH}: {e}")
|
sys.exit(f"Error writing to {NODES_JSON_PATH}: {e}")
|
||||||
|
|
||||||
def add_node(name: str, ip: str):
|
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()
|
nodes = read_nodes()
|
||||||
if any(node['name'] == name for node in nodes):
|
if any(node['name'] == name for node in nodes):
|
||||||
print(f"Error: A node with the name '{name}' already exists.", file=sys.stderr)
|
print(f"Error: A node with the name '{name}' already exists.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if any(node['ip'] == ip for node in nodes):
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
nodes.append({"name": name, "ip": ip})
|
nodes.append({"name": name, "ip": ip})
|
||||||
write_nodes(nodes)
|
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):
|
def delete_node(name: str):
|
||||||
nodes = read_nodes()
|
nodes = read_nodes()
|
||||||
@ -66,10 +87,10 @@ def list_nodes():
|
|||||||
print("No nodes configured.")
|
print("No nodes configured.")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"{'Name':<20} {'IP Address'}")
|
print(f"{'Name':<30} {'IP Address / Domain'}")
|
||||||
print(f"{'-'*20} {'-'*15}")
|
print(f"{'-'*30} {'-'*25}")
|
||||||
for node in sorted(nodes, key=lambda x: x['name']):
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Manage external node configurations.")
|
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 = 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('--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 = 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.')
|
delete_parser.add_argument('--name', type=str, required=True, help='The name of the node to delete.')
|
||||||
|
|||||||
@ -1,24 +1,32 @@
|
|||||||
from pydantic import BaseModel, field_validator, ValidationInfo
|
from pydantic import BaseModel, field_validator, ValidationInfo
|
||||||
from ipaddress import IPv4Address, IPv6Address, ip_address
|
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):
|
class StatusResponse(BaseModel):
|
||||||
ipv4: str | None = None
|
ipv4: str | None = None
|
||||||
ipv6: str | None = None
|
ipv6: str | None = None
|
||||||
|
|
||||||
@field_validator('ipv4', 'ipv6', mode='before')
|
@field_validator('ipv4', 'ipv6', mode='before')
|
||||||
def check_ip_or_domain(cls, v: str, info: ValidationInfo):
|
def check_local_server_ip(cls, v: str | None):
|
||||||
if v is None:
|
return validate_ip_or_domain(v)
|
||||||
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")
|
|
||||||
|
|
||||||
class EditInputBody(StatusResponse):
|
class EditInputBody(StatusResponse):
|
||||||
pass
|
pass
|
||||||
@ -28,14 +36,10 @@ class Node(BaseModel):
|
|||||||
ip: str
|
ip: str
|
||||||
|
|
||||||
@field_validator('ip', mode='before')
|
@field_validator('ip', mode='before')
|
||||||
def check_ip(cls, v: str, info: ValidationInfo):
|
def check_node_ip(cls, v: str | None):
|
||||||
if v is None:
|
if not v or not v.strip():
|
||||||
raise ValueError("IP cannot be None")
|
raise ValueError("IP or Domain field cannot be empty.")
|
||||||
try:
|
return validate_ip_or_domain(v)
|
||||||
ip_address(v)
|
|
||||||
return v
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(f"'{v}' is not a valid IPv4 or IPv6 address")
|
|
||||||
|
|
||||||
class AddNodeBody(Node):
|
class AddNodeBody(Node):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -49,7 +49,7 @@
|
|||||||
<li class='nav-item'>
|
<li class='nav-item'>
|
||||||
<a class='nav-link' id='ip-tab' data-toggle='pill' href='#change_ip' role='tab'
|
<a class='nav-link' id='ip-tab' data-toggle='pill' href='#change_ip' role='tab'
|
||||||
aria-controls='change_ip' aria-selected='false'><i class="fas fa-network-wired"></i>
|
aria-controls='change_ip' aria-selected='false'><i class="fas fa-network-wired"></i>
|
||||||
Change IP</a>
|
IP Management</a>
|
||||||
</li>
|
</li>
|
||||||
<li class='nav-item'>
|
<li class='nav-item'>
|
||||||
<a class='nav-link' id='backup-tab' data-toggle='pill' href='#backup' role='tab'
|
<a class='nav-link' id='backup-tab' data-toggle='pill' href='#backup' role='tab'
|
||||||
@ -208,28 +208,69 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change IP Tab -->
|
<!-- IP Management Tab -->
|
||||||
<div class='tab-pane fade' id='change_ip' role='tabpanel' aria-labelledby='ip-tab'>
|
<div class='tab-pane fade' id='change_ip' role='tabpanel' aria-labelledby='ip-tab'>
|
||||||
|
<div class="card card-outline card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Local Server IP / Domain</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<form id="change_ip_form">
|
<form id="change_ip_form">
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label for='ipv4'>IPv4:</label>
|
<label for='ipv4'>IPv4 / Domain:</label>
|
||||||
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4 or Domain'
|
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4 or Domain' value="{{ ipv4 or '' }}">
|
||||||
value="{{ ipv4 or '' }}">
|
<div class="invalid-feedback">Please enter a valid IPv4 address or Domain.</div>
|
||||||
<div class="invalid-feedback">
|
|
||||||
Please enter a valid IPv4 address or Domain.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label for='ipv6'>IPv6:</label>
|
<label for='ipv6'>IPv6 / Domain:</label>
|
||||||
<input type='text' class='form-control' id='ipv6' placeholder='Enter IPv6 or Domain'
|
<input type='text' class='form-control' id='ipv6' placeholder='Enter IPv6 or Domain' value="{{ ipv6 or '' }}">
|
||||||
value="{{ ipv6 or '' }}">
|
<div class="invalid-feedback">Please enter a valid IPv6 address or Domain.</div>
|
||||||
<div class="invalid-feedback">
|
|
||||||
Please enter a valid IPv6 address or Domain.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="ip_change" type='button' class='btn btn-primary'>Save</button>
|
<button id="ip_change" type='button' class='btn btn-primary'>Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-outline card-secondary mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">External Nodes</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-bordered table-striped" id="nodes_table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Node Name</th>
|
||||||
|
<th>IP Address / Domain</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="no_nodes_message" class="alert alert-info" style="display: none;">
|
||||||
|
No external nodes have been configured.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h5>Add New Node</h5>
|
||||||
|
<form id="add_node_form" class="form-inline">
|
||||||
|
<div class="form-group mb-2 mr-sm-2">
|
||||||
|
<label for="node_name" class="sr-only">Node Name</label>
|
||||||
|
<input type="text" class="form-control" id="node_name" placeholder="e.g., Node-US">
|
||||||
|
<div class="invalid-feedback">Please enter a name.</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-2 mr-sm-2">
|
||||||
|
<label for="node_ip" class="sr-only">IP Address / Domain</label>
|
||||||
|
<input type="text" class="form-control" id="node_ip" placeholder="e.g., node.example.com">
|
||||||
|
<div class="invalid-feedback">Please enter a valid IP or domain.</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="add_node_btn" class="btn btn-success mb-2">
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
|
||||||
|
Add Node
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Backup Tab -->
|
<!-- Backup Tab -->
|
||||||
<div class='tab-pane fade' id='backup' role='tabpanel' aria-labelledby='backup-tab'>
|
<div class='tab-pane fade' id='backup' role='tabpanel' aria-labelledby='backup-tab'>
|
||||||
@ -414,7 +455,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /.card -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -436,6 +476,7 @@
|
|||||||
initUI();
|
initUI();
|
||||||
fetchDecoyStatus();
|
fetchDecoyStatus();
|
||||||
fetchObfsStatus();
|
fetchObfsStatus();
|
||||||
|
fetchNodes();
|
||||||
|
|
||||||
function isValidPath(path) {
|
function isValidPath(path) {
|
||||||
if (!path) return false;
|
if (!path) return false;
|
||||||
@ -458,20 +499,17 @@
|
|||||||
return /^[a-zA-Z0-9]+$/.test(subpath);
|
return /^[a-zA-Z0-9]+$/.test(subpath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function isValidIPorDomain(input) {
|
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 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 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 domainRegex = /^(?!-)(?:[a-zA-Z\d-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+$)[a-zA-Z\d]{1,63}$/;
|
||||||
const lowerInput = input.toLowerCase();
|
const lowerInput = input.toLowerCase();
|
||||||
|
|
||||||
if (ipV4Regex.test(input)) return true;
|
return ipV4Regex.test(input) || ipV6Regex.test(input) || domainRegex.test(lowerInput);
|
||||||
if (ipV6Regex.test(input)) return true;
|
|
||||||
if (domainRegex.test(lowerInput) && !lowerInput.startsWith("http://") && !lowerInput.startsWith("https://")) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidPositiveNumber(value) {
|
function isValidPositiveNumber(value) {
|
||||||
@ -518,12 +556,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log("Success Response:", response);
|
|
||||||
},
|
},
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
let errorMessage = "Something went wrong.";
|
let errorMessage = "An unexpected error occurred.";
|
||||||
if (xhr.responseJSON && xhr.responseJSON.detail) {
|
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");
|
Swal.fire("Error!", errorMessage, "error");
|
||||||
console.error("AJAX Error:", status, error, xhr.responseText);
|
console.error("AJAX Error:", status, error, xhr.responseText);
|
||||||
@ -552,6 +594,10 @@
|
|||||||
fieldValid = isValidSubPath(input.val());
|
fieldValid = isValidSubPath(input.val());
|
||||||
} else if (id === 'ipv4' || id === 'ipv6') {
|
} else if (id === 'ipv4' || id === 'ipv6') {
|
||||||
fieldValid = (input.val().trim() === '') ? true : isValidIPorDomain(input.val());
|
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') {
|
} else if (id === 'block_duration' || id === 'max_ips') {
|
||||||
fieldValid = isValidPositiveNumber(input.val());
|
fieldValid = isValidPositiveNumber(input.val());
|
||||||
} else if (id === 'decoy_path') {
|
} else if (id === 'decoy_path') {
|
||||||
@ -572,7 +618,6 @@
|
|||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function initUI() {
|
function initUI() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{{ url_for('server_services_status_api') }}",
|
url: "{{ url_for('server_services_status_api') }}",
|
||||||
@ -621,6 +666,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 = `<tr>
|
||||||
|
<td>${node.name}</td>
|
||||||
|
<td>${node.ip}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-xs btn-danger delete-node-btn" data-name="${node.name}">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
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) {
|
function updateServiceUI(data) {
|
||||||
const servicesMap = {
|
const servicesMap = {
|
||||||
"hysteria_telegram_bot": "#telegram_form",
|
"hysteria_telegram_bot": "#telegram_form",
|
||||||
@ -633,7 +755,6 @@
|
|||||||
let targetSelector = servicesMap[serviceKey];
|
let targetSelector = servicesMap[serviceKey];
|
||||||
let isRunning = data[serviceKey];
|
let isRunning = data[serviceKey];
|
||||||
|
|
||||||
|
|
||||||
if (serviceKey === "hysteria_normal_sub") {
|
if (serviceKey === "hysteria_normal_sub") {
|
||||||
const $normalForm = $("#normal_sub_service_form");
|
const $normalForm = $("#normal_sub_service_form");
|
||||||
const $normalFormGroups = $normalForm.find(".form-group");
|
const $normalFormGroups = $normalForm.find(".form-group");
|
||||||
@ -771,7 +892,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function setupDecoy() {
|
function setupDecoy() {
|
||||||
if (!validateForm('decoy_form')) return;
|
if (!validateForm('decoy_form')) return;
|
||||||
const domain = $("#decoy_domain").val();
|
const domain = $("#decoy_domain").val();
|
||||||
@ -846,7 +966,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function fetchObfsStatus() {
|
function fetchObfsStatus() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{{ url_for('check_obfs') }}",
|
url: "{{ url_for('check_obfs') }}",
|
||||||
@ -908,7 +1027,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function startTelegram() {
|
function startTelegram() {
|
||||||
if (!validateForm('telegram_form')) return;
|
if (!validateForm('telegram_form')) return;
|
||||||
const apiToken = $("#telegram_api_token").val();
|
const apiToken = $("#telegram_api_token").val();
|
||||||
@ -1197,7 +1315,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
$("#telegram_start").on("click", startTelegram);
|
$("#telegram_start").on("click", startTelegram);
|
||||||
$("#telegram_stop").on("click", stopTelegram);
|
$("#telegram_stop").on("click", stopTelegram);
|
||||||
$("#normal_start").on("click", startNormal);
|
$("#normal_start").on("click", startNormal);
|
||||||
@ -1215,7 +1332,11 @@
|
|||||||
$("#decoy_stop").on("click", stopDecoy);
|
$("#decoy_stop").on("click", stopDecoy);
|
||||||
$("#obfs_enable_btn").on("click", enableObfs);
|
$("#obfs_enable_btn").on("click", enableObfs);
|
||||||
$("#obfs_disable_btn").on("click", disableObfs);
|
$("#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 () {
|
$('#normal_domain, #sni_domain, #decoy_domain').on('input', function () {
|
||||||
if (isValidDomain($(this).val())) {
|
if (isValidDomain($(this).val())) {
|
||||||
@ -1247,8 +1368,19 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#ipv4, #ipv6').on('input', function () {
|
$('#ipv4, #ipv6, #node_ip').on('input', function () {
|
||||||
if (isValidIPorDomain($(this).val()) || $(this).val().trim() === '') {
|
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');
|
$(this).removeClass('is-invalid');
|
||||||
} else {
|
} else {
|
||||||
$(this).addClass('is-invalid');
|
$(this).addClass('is-invalid');
|
||||||
|
|||||||
Reference in New Issue
Block a user