Merge pull request #115 from ReturnFI/beta
feat: allow updating IPv4, IPv6, or domain in IP Address Manager
This commit is contained in:
@ -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
|
||||
|
||||
@ -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=""
|
||||
|
||||
@ -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
|
||||
@ -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');
|
||||
|
||||
@ -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
35
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
|
||||
;;
|
||||
|
||||
Reference in New Issue
Block a user