diff --git a/changelog b/changelog index a1270a5..2953165 100644 --- a/changelog +++ b/changelog @@ -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 diff --git a/core/scripts/hysteria2/show_user_uri.sh b/core/scripts/hysteria2/show_user_uri.sh index c86dc50..3ba648d 100644 --- a/core/scripts/hysteria2/show_user_uri.sh +++ b/core/scripts/hysteria2/show_user_uri.sh @@ -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="" 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 81a5667..adfa1ad 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/config/ip.py +++ b/core/scripts/webpanel/routers/api/v1/schema/config/ip.py @@ -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 \ No newline at end of file diff --git a/core/scripts/webpanel/templates/settings.html b/core/scripts/webpanel/templates/settings.html index 2ade890..3d30a40 100644 --- a/core/scripts/webpanel/templates/settings.html +++ b/core/scripts/webpanel/templates/settings.html @@ -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'); diff --git a/core/scripts/webpanel/templates/users.html b/core/scripts/webpanel/templates/users.html index 619af73..096e618 100644 --- a/core/scripts/webpanel/templates/users.html +++ b/core/scripts/webpanel/templates/users.html @@ -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 = $(` -
+
-
-
${configType}
+
+
+
${displayType}
-
+
`); 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(); diff --git a/menu.sh b/menu.sh index 17e8680..b39960c 100644 --- a/menu.sh +++ b/menu.sh @@ -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 ;;