Merge pull request #115 from ReturnFI/beta

feat: allow updating IPv4, IPv6, or domain in IP Address Manager
This commit is contained in:
Whispering Wind
2025-03-21 21:18:53 +03:30
committed by GitHub
6 changed files with 101 additions and 77 deletions

View File

@ -1,3 +1 @@
🚀 feat: Add version check and notification new releases
🛠️ refactor: Improve utils package structure with cleaner imports
🐛 fix: IPv6 validation pattern
🚀 feat: allow updating IPv4, IPv6, or domain in IP Address Manager

View File

@ -16,7 +16,7 @@ get_singbox_domain_and_port() {
get_normalsub_domain_and_port() {
if [ -f "$NORMALSUB_ENV" ]; then
local domain port
local domain port subpath
domain=$(grep -E '^HYSTERIA_DOMAIN=' "$NORMALSUB_ENV" | cut -d'=' -f2)
port=$(grep -E '^HYSTERIA_PORT=' "$NORMALSUB_ENV" | cut -d'=' -f2)
subpath=$(grep -E '^SUBPATH=' "$NORMALSUB_ENV" | cut -d'=' -f2)
@ -81,7 +81,11 @@ show_uri() {
local uri_base="hy2://$username%3A$authpassword@$ip:$port"
if [ "$ip_version" -eq 6 ]; then
uri_base="hy2://$username%3A$authpassword@[$ip]:$port"
if [[ "$ip" =~ ^[0-9a-fA-F:]+$ ]]; then
uri_base="hy2://$username%3A$authpassword@[$ip]:$port"
else
uri_base="hy2://$username%3A$authpassword@$ip:$port"
fi
fi
local params=""

View File

@ -1,11 +1,24 @@
from pydantic import BaseModel
from ipaddress import IPv4Address, IPv6Address
from pydantic import BaseModel, field_validator, ValidationInfo
from ipaddress import IPv4Address, IPv6Address, ip_address
import socket
class StatusResponse(BaseModel):
ipv4: IPv4Address | None = None
ipv6: IPv6Address | None = None
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")
class EditInputBody(StatusResponse):
pass
pass

View File

@ -264,14 +264,18 @@
if (!port) return false;
return /^[0-9]+$/.test(port) && parseInt(port) > 0 && parseInt(port) <= 65535;
}
function isValidIP(ip, version) {
if (!ip) return true;
function isValidIPorDomain(input) {
if (!input) return true;
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;
if (version === 4) {
return /^(?:(?: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]?)$/.test(ip);
} else if (version === 6) {
return /^(([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}|:))$/.test(ip);
}
return false;
}
@ -352,15 +356,8 @@
} else {
input.removeClass('is-invalid');
}
} else if (id === 'ipv4') {
if (!isValidIP(input.val(), 4)) {
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
} else if (id === 'ipv6') {
if (!isValidIP(input.val(), 6)) {
} else if (id === 'ipv4' || id === 'ipv6') { // Apply isValidIPorDomain for both IPv4 and IPv6
if (!isValidIPorDomain(input.val())) {
input.addClass('is-invalid');
isValid = false;
} else {
@ -447,15 +444,15 @@
$("#ipv4").val(data.ipv4 || "");
$("#ipv6").val(data.ipv6 || "");
$("#ipv4").attr("placeholder", "Enter IPv4");
$("#ipv6").attr("placeholder", "Enter IPv6");
$("#ipv4").attr("placeholder", "Enter IPv4 or Domain");
$("#ipv6").attr("placeholder", "Enter IPv6 or Domain");
},
error: function () {
console.error("Failed to fetch IP addresses.");
$("#ipv4").attr("placeholder", "Enter IPv4");
$("#ipv6").attr("placeholder", "Enter IPv6");
$("#ipv4").attr("placeholder", "Enter IPv4 or Domain");
$("#ipv6").attr("placeholder", "Enter IPv6 or Domain");
}
});
@ -695,21 +692,14 @@
$(this).addClass('is-invalid');
}
});
$('#ipv4').on('input', function () {
if (isValidIP($(this).val(), 4)) {
$('#ipv4, #ipv6').on('input', function () { // Apply to both ipv4 and ipv6
if (isValidIPorDomain($(this).val())) {
$(this).removeClass('is-invalid');
} else {
$(this).addClass('is-invalid');
}
});
$('#ipv6').on('input', function () {
if (isValidIP($(this).val(), 6)) {
$(this).removeClass('is-invalid');
} else {
$(this).addClass('is-invalid');
}
});
$('#telegram_api_token, #telegram_admin_id').on('input', function () {
if ($(this).val().trim() !== "") {
$(this).removeClass('is-invalid');

View File

@ -678,32 +678,48 @@
// QR Code Modal
$("#qrcodeModal").on("show.bs.modal", function (event) {
const button = $(event.relatedTarget);
const username = button.data("username");
const configContainer = $(`#userConfigs-${username}`);
const qrcodesContainer = $("#qrcodesContainer");
qrcodesContainer.empty();
const button = $(event.relatedTarget);
const configContainer = $(`#userConfigs-${button.data("username")}`);
const qrcodesContainer = $("#qrcodesContainer");
qrcodesContainer.empty();
configContainer.find(".config-container").each(function () {
const configLink = $(this).data("link");
const configType = $(this).find(".config-type").text().replace(":", "");
// Create a card for each QR code
configContainer.find(".config-container").each(function () {
const configLink = $(this).data("link");
const configType = $(this).find(".config-type").text().replace(":", "");
let displayType = configType;
const hashMatch = configLink.match(/#(.+)$/);
if (hashMatch && hashMatch[1]) {
const hashValue = hashMatch[1];
if (hashValue.includes("IPv4") || hashValue.includes("IPv6")) {
displayType = hashValue;
}
} else if (configLink.includes("ipv4") || configLink.includes("IPv4")) {
displayType = "IPv4";
} else if (configLink.includes("ipv6") || configLink.includes("IPv6")) {
displayType = "IPv6";
}
const qrCodeId = `qrcode-${displayType}-${Math.random().toString(36).substring(2, 10)}`;
const card = $(`
<div class="card d-inline-block mx-2 my-2" style="width: 180px;">
<div class="card d-inline-block my-2">
<div class="card-body">
<div id="qrcode-${configType}" class="mx-auto cursor-pointer"></div>
<div class="config-type-text mt-2 text-center">${configType}</div>
<div id="${qrCodeId}" class="mx-auto cursor-pointer"></div>
<br>
<div class="config-type-text mt-2 text-center">${displayType}</div>
</div>
</div>
</div>
`);
qrcodesContainer.append(card);
const qrCodeStyling = new QRCodeStyling({
width: 150,
height: 150,
width: 180,
height: 180,
data: configLink,
margin: 5,
dotsOptions: {
color: "#212121",
type: "square"
@ -720,15 +736,14 @@
}
});
qrCodeStyling.append(document.getElementById(`qrcode-${configType}`));
// Add click to copy functionality to the card
qrCodeStyling.append(document.getElementById(qrCodeId));
card.on("click", function () {
navigator.clipboard.writeText(configLink)
.then(() => {
Swal.fire({
icon: "success",
title: configType + " link copied!",
title: displayType + " link copied!",
showConfirmButton: false,
timer: 1500,
});
@ -745,12 +760,10 @@
});
});
// Prevent modal from closing when clicking inside
$("#qrcodeModal .modal-content").on("click", function (e) {
e.stopPropagation();
});
// Clear the QR code when the modal is hidden
$("#qrcodeModal").on("hidden.bs.modal", function () {
$("#qrcodesContainer").empty();
});
@ -759,7 +772,6 @@
$("#qrcodeModal").modal("hide");
});
// Search Functionality
function filterUsers() {
const searchText = $("#searchInput").val().toLowerCase();

35
menu.sh
View File

@ -323,35 +323,42 @@ hysteria2_change_sni_handler() {
edit_ips() {
while true; do
echo "======================================"
echo " IP Address Manager "
echo " IP/Domain Address Manager "
echo "======================================"
echo "1. Change IP4"
echo "2. Change IP6"
echo "1. Change IPv4 or Domain"
echo "2. Change IPv6 or Domain"
echo "0. Back"
echo "======================================"
read -p "Enter your choice [1-3]: " choice
read -p "Enter your choice [0-2]: " choice
case $choice in
1)
read -p "Enter the new IPv4 address: " new_ip4
if [[ $new_ip4 =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
if [[ $(echo "$new_ip4" | awk -F. '{for (i=1;i<=NF;i++) if ($i>255) exit 1}') ]]; then
read -p "Enter the new IPv4 address or domain: " new_ip4_or_domain
if [[ $new_ip4_or_domain =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
if [[ $(echo "$new_ip4_or_domain" | awk -F. '{for (i=1;i<=NF;i++) if ($i>255) exit 1}') ]]; then
echo "Error: Invalid IPv4 address. Values must be between 0 and 255."
else
python3 "$CLI_PATH" ip-address --edit -4 "$new_ip4"
python3 "$CLI_PATH" ip-address --edit -4 "$new_ip4_or_domain"
echo "IPv4 address has been updated to $new_ip4_or_domain."
fi
elif [[ $new_ip4_or_domain =~ ^[a-zA-Z0-9.-]+$ ]] && [[ ! $new_ip4_or_domain =~ [/:] ]]; then
python3 "$CLI_PATH" ip-address --edit -4 "$new_ip4_or_domain"
echo "Domain has been updated to $new_ip4_or_domain."
else
echo "Error: Invalid IPv4 address format."
echo "Error: Invalid IPv4 or domain format."
fi
break
;;
2)
read -p "Enter the new IPv6 address: " new_ip6
if [[ $new_ip6 =~ ^(([0-9a-fA-F]{1,4}:){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}|:))$ ]]; then
python3 "$CLI_PATH" ip-address --edit -6 "$new_ip6"
echo "IPv6 address has been updated to $new_ip6."
read -p "Enter the new IPv6 address or domain: " new_ip6_or_domain
if [[ $new_ip6_or_domain =~ ^(([0-9a-fA-F]{1,4}:){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}|:))$ ]]; then
python3 "$CLI_PATH" ip-address --edit -6 "$new_ip6_or_domain"
echo "IPv6 address has been updated to $new_ip6_or_domain."
elif [[ $new_ip6_or_domain =~ ^[a-zA-Z0-9.-]+$ ]] && [[ ! $new_ip6_or_domain =~ [/:] ]]; then
python3 "$CLI_PATH" ip-address --edit -6 "$new_ip6_or_domain"
echo "Domain has been updated to $new_ip6_or_domain."
else
echo "Error: Invalid IPv6 address format."
echo "Error: Invalid IPv6 or domain format."
fi
break
;;