mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-13 13:57:59 +00:00
Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5c0fe3edf | ||
|
|
f4057989f5 |
7 changed files with 247 additions and 25 deletions
|
|
@ -654,8 +654,11 @@ config_after_install() {
|
|||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||
local http_code=$(echo "$response" | tail -n1)
|
||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||
server_ip="${ip_result}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
|
|
|||
|
|
@ -687,8 +687,11 @@ config_after_update() {
|
|||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(${curl_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||
local http_code=$(echo "$response" | tail -n1)
|
||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||
server_ip="${ip_result}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
|
@ -193,6 +194,37 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Prefer returning a normalized string list for consistent UI rendering
|
||||
type ipWithTimestamp struct {
|
||||
IP string `json:"ip"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
var ipsWithTime []ipWithTimestamp
|
||||
if err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {
|
||||
formatted := make([]string, 0, len(ipsWithTime))
|
||||
for _, item := range ipsWithTime {
|
||||
if item.IP == "" {
|
||||
continue
|
||||
}
|
||||
if item.Timestamp > 0 {
|
||||
ts := time.Unix(item.Timestamp, 0).Local().Format("2006-01-02 15:04:05")
|
||||
formatted = append(formatted, fmt.Sprintf("%s (%s)", item.IP, ts))
|
||||
continue
|
||||
}
|
||||
formatted = append(formatted, item.IP)
|
||||
}
|
||||
jsonObj(c, formatted, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var oldIps []string
|
||||
if err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {
|
||||
jsonObj(c, oldIps, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// If parsing fails, return as string
|
||||
jsonObj(c, ips, nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,15 +260,31 @@
|
|||
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
|
||||
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ infoModal.clientIps ]]</a-tag>
|
||||
<a-icon type="sync" :spin="refreshing" @click="refreshIPs"
|
||||
:style="{ margin: '0 5px' }"></a-icon>
|
||||
<a-tooltip :title="[[ dbInbound.address ]]">
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||
</template>
|
||||
<a-icon type="delete" @click="clearClientIps"></a-icon>
|
||||
</a-tooltip>
|
||||
<div
|
||||
style="max-height: 150px; overflow-y: auto; text-align: left;">
|
||||
<div
|
||||
v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
|
||||
<a-tag
|
||||
v-for="(ipInfo, idx) in infoModal.clientIpsArray"
|
||||
:key="idx"
|
||||
color="blue"
|
||||
style="margin: 2px 0; display: block; font-family: monospace; font-size: 11px;">
|
||||
[[ formatIpInfo(ipInfo) ]]
|
||||
</a-tag>
|
||||
</div>
|
||||
<a-tag v-else>[[ infoModal.clientIps || 'No IP Record'
|
||||
]]</a-tag>
|
||||
</div>
|
||||
<div style="margin-top: 5px;">
|
||||
<a-icon type="sync" :spin="refreshing" @click="refreshIPs"
|
||||
:style="{ margin: '0 5px' }"></a-icon>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||
</template>
|
||||
<a-icon type="delete" @click="clearClientIps"></a-icon>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -542,12 +558,73 @@
|
|||
<script>
|
||||
function refreshIPs(email) {
|
||||
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
||||
if (msg.success) {
|
||||
try {
|
||||
return JSON.parse(msg.obj).join(', ');
|
||||
} catch (e) {
|
||||
return msg.obj;
|
||||
if (!msg.success) {
|
||||
return { text: 'No IP Record', array: [] };
|
||||
}
|
||||
|
||||
const formatIpRecord = (record) => {
|
||||
if (record == null) {
|
||||
return '';
|
||||
}
|
||||
if (typeof record === 'string' || typeof record === 'number') {
|
||||
return String(record);
|
||||
}
|
||||
const ip = record.ip || record.IP || '';
|
||||
const timestamp = record.timestamp || record.Timestamp || 0;
|
||||
if (!ip) {
|
||||
return String(record);
|
||||
}
|
||||
if (!timestamp) {
|
||||
return String(ip);
|
||||
}
|
||||
const date = new Date(Number(timestamp) * 1000);
|
||||
const timeStr = date
|
||||
.toLocaleString('en-GB', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.replace(',', '');
|
||||
return `${ip} (${timeStr})`;
|
||||
};
|
||||
|
||||
try {
|
||||
let ips = msg.obj;
|
||||
// If msg.obj is a string, try to parse it
|
||||
if (typeof ips === 'string') {
|
||||
try {
|
||||
ips = JSON.parse(ips);
|
||||
} catch (e) {
|
||||
return { text: String(ips), array: [String(ips)] };
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize single object response to array
|
||||
if (ips && !Array.isArray(ips) && typeof ips === 'object') {
|
||||
ips = [ips];
|
||||
}
|
||||
|
||||
// New format or object array
|
||||
if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
|
||||
const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
|
||||
return { text: result.join(' | '), array: result };
|
||||
}
|
||||
|
||||
// Old format - simple array of IPs
|
||||
if (Array.isArray(ips) && ips.length > 0) {
|
||||
const result = ips.map((ip) => String(ip));
|
||||
return { text: result.join(', '), array: result };
|
||||
}
|
||||
|
||||
// Fallback for any other format
|
||||
return { text: String(ips), array: [String(ips)] };
|
||||
|
||||
} catch (e) {
|
||||
return { text: 'Error loading IPs', array: [] };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -566,6 +643,7 @@
|
|||
subLink: '',
|
||||
subJsonLink: '',
|
||||
clientIps: '',
|
||||
clientIpsArray: [],
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
|
|
@ -583,8 +661,9 @@
|
|||
].includes(this.inbound.protocol)
|
||||
) {
|
||||
if (app.ipLimitEnable && this.clientSettings.limitIp) {
|
||||
refreshIPs(this.clientStats.email).then((ips) => {
|
||||
this.clientIps = ips;
|
||||
refreshIPs(this.clientStats.email).then((result) => {
|
||||
this.clientIps = result.text;
|
||||
this.clientIpsArray = result.array;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -655,6 +734,35 @@
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
formatIpInfo(ipInfo) {
|
||||
if (ipInfo == null) {
|
||||
return '';
|
||||
}
|
||||
if (typeof ipInfo === 'string' || typeof ipInfo === 'number') {
|
||||
return String(ipInfo);
|
||||
}
|
||||
const ip = ipInfo.ip || ipInfo.IP || '';
|
||||
const timestamp = ipInfo.timestamp || ipInfo.Timestamp || 0;
|
||||
if (!ip) {
|
||||
return String(ipInfo);
|
||||
}
|
||||
if (!timestamp) {
|
||||
return String(ip);
|
||||
}
|
||||
const date = new Date(Number(timestamp) * 1000);
|
||||
const timeStr = date
|
||||
.toLocaleString('en-GB', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.replace(',', '');
|
||||
return `${ip} (${timeStr})`;
|
||||
},
|
||||
copy(content) {
|
||||
ClipboardManager
|
||||
.copyText(content)
|
||||
|
|
@ -672,8 +780,9 @@
|
|||
refreshIPs() {
|
||||
this.refreshing = true;
|
||||
refreshIPs(this.infoModal.clientStats.email)
|
||||
.then((ips) => {
|
||||
this.infoModal.clientIps = ips;
|
||||
.then((result) => {
|
||||
this.infoModal.clientIps = result.text;
|
||||
this.infoModal.clientIpsArray = result.array;
|
||||
})
|
||||
.finally(() => {
|
||||
this.refreshing = false;
|
||||
|
|
@ -686,6 +795,7 @@
|
|||
return;
|
||||
}
|
||||
this.infoModal.clientIps = 'No IP Record';
|
||||
this.infoModal.clientIpsArray = [];
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2141,6 +2141,43 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if InboundClientIps.Ips == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Try to parse as new format (with timestamps)
|
||||
type IPWithTimestamp struct {
|
||||
IP string `json:"ip"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
var ipsWithTime []IPWithTimestamp
|
||||
err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
|
||||
|
||||
// If successfully parsed as new format, return with timestamps
|
||||
if err == nil && len(ipsWithTime) > 0 {
|
||||
return InboundClientIps.Ips, nil
|
||||
}
|
||||
|
||||
// Otherwise, assume it's old format (simple string array)
|
||||
// Try to parse as simple array and convert to new format
|
||||
var oldIps []string
|
||||
err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)
|
||||
if err == nil && len(oldIps) > 0 {
|
||||
// Convert old format to new format with current timestamp
|
||||
newIpsWithTime := make([]IPWithTimestamp, len(oldIps))
|
||||
for i, ip := range oldIps {
|
||||
newIpsWithTime[i] = IPWithTimestamp{
|
||||
IP: ip,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
result, _ := json.Marshal(newIpsWithTime)
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// Return as-is if parsing fails
|
||||
return InboundClientIps.Ips, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/rand"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -3083,9 +3084,41 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
|||
ips = t.I18nBot("tgbot.noIpRecord")
|
||||
}
|
||||
|
||||
formattedIps := ips
|
||||
if err == nil && len(ips) > 0 {
|
||||
type ipWithTimestamp struct {
|
||||
IP string `json:"ip"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
var ipsWithTime []ipWithTimestamp
|
||||
if json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 {
|
||||
lines := make([]string, 0, len(ipsWithTime))
|
||||
for _, item := range ipsWithTime {
|
||||
if item.IP == "" {
|
||||
continue
|
||||
}
|
||||
if item.Timestamp > 0 {
|
||||
ts := time.Unix(item.Timestamp, 0).Format("2006-01-02 15:04:05")
|
||||
lines = append(lines, fmt.Sprintf("%s (%s)", item.IP, ts))
|
||||
continue
|
||||
}
|
||||
lines = append(lines, item.IP)
|
||||
}
|
||||
if len(lines) > 0 {
|
||||
formattedIps = strings.Join(lines, "\n")
|
||||
}
|
||||
} else {
|
||||
var oldIps []string
|
||||
if json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 {
|
||||
formattedIps = strings.Join(oldIps, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output := ""
|
||||
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
||||
output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips)
|
||||
output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps)
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
|
|
|
|||
8
x-ui.sh
8
x-ui.sh
|
|
@ -2062,11 +2062,15 @@ SSH_port_forwarding() {
|
|||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||
local http_code=$(echo "$response" | tail -n1)
|
||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||
server_ip="${ip_result}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')
|
||||
|
|
|
|||
Loading…
Reference in a new issue