fix: Resolve IP/domain validation
This commit is contained in:
@ -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
|
||||
|
||||
@ -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 %}
|
||||
Reference in New Issue
Block a user