fix: Resolve IP/domain validation

This commit is contained in:
Whispering Wind
2025-08-06 15:16:27 +03:30
committed by GitHub
parent 05993d013d
commit 133684f4fe
3 changed files with 229 additions and 72 deletions

View File

@ -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

View File

@ -49,7 +49,7 @@
<li class='nav-item'>
<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>
Change IP</a>
IP Management</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='backup-tab' data-toggle='pill' href='#backup' role='tab'
@ -208,27 +208,68 @@
</button>
</div>
<!-- Change IP Tab -->
<!-- IP Management Tab -->
<div class='tab-pane fade' id='change_ip' role='tabpanel' aria-labelledby='ip-tab'>
<form id="change_ip_form">
<div class='form-group'>
<label for='ipv4'>IPv4:</label>
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4 or Domain'
value="{{ ipv4 or '' }}">
<div class="invalid-feedback">
Please enter a valid IPv4 address or Domain.
</div>
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Local Server IP / Domain</h3>
</div>
<div class='form-group'>
<label for='ipv6'>IPv6:</label>
<input type='text' class='form-control' id='ipv6' placeholder='Enter IPv6 or Domain'
value="{{ ipv6 or '' }}">
<div class="invalid-feedback">
Please enter a valid IPv6 address or Domain.
</div>
<div class="card-body">
<form id="change_ip_form">
<div class='form-group'>
<label for='ipv4'>IPv4 / Domain:</label>
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4 or Domain' value="{{ ipv4 or '' }}">
<div class="invalid-feedback">Please enter a valid IPv4 address or Domain.</div>
</div>
<div class='form-group'>
<label for='ipv6'>IPv6 / Domain:</label>
<input type='text' class='form-control' id='ipv6' placeholder='Enter IPv6 or Domain' value="{{ ipv6 or '' }}">
<div class="invalid-feedback">Please enter a valid IPv6 address or Domain.</div>
</div>
<button id="ip_change" type='button' class='btn btn-primary'>Save</button>
</form>
</div>
<button id="ip_change" type='button' class='btn btn-primary'>Save</button>
</form>
</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 -->
@ -414,7 +455,6 @@
</div>
</div>
<!-- /.card -->
</div>
</div>
</div>
@ -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 = `<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) {
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 @@
});
</script>
{% endblock %}
{% endblock %}