mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-13 13:57:59 +00:00
Compare commits
No commits in common. "main" and "v2.8.10" have entirely different histories.
7 changed files with 25 additions and 247 deletions
|
|
@ -654,11 +654,8 @@ config_after_install() {
|
||||||
)
|
)
|
||||||
local server_ip=""
|
local server_ip=""
|
||||||
for ip_address in "${URL_lists[@]}"; do
|
for ip_address in "${URL_lists[@]}"; do
|
||||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||||
local http_code=$(echo "$response" | tail -n1)
|
if [[ -n "${server_ip}" ]]; then
|
||||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
|
||||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
|
||||||
server_ip="${ip_result}"
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -687,11 +687,8 @@ config_after_update() {
|
||||||
)
|
)
|
||||||
local server_ip=""
|
local server_ip=""
|
||||||
for ip_address in "${URL_lists[@]}"; do
|
for ip_address in "${URL_lists[@]}"; do
|
||||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
server_ip=$(${curl_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||||
local http_code=$(echo "$response" | tail -n1)
|
if [[ -n "${server_ip}" ]]; then
|
||||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
|
||||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
|
||||||
server_ip="${ip_result}"
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
@ -194,37 +193,6 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
||||||
return
|
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)
|
jsonObj(c, ips, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -260,31 +260,15 @@
|
||||||
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
|
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
|
||||||
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
|
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<a-tag>[[ infoModal.clientIps ]]</a-tag>
|
||||||
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"
|
<a-icon type="sync" :spin="refreshing" @click="refreshIPs"
|
||||||
:style="{ margin: '0 5px' }"></a-icon>
|
:style="{ margin: '0 5px' }"></a-icon>
|
||||||
<a-tooltip>
|
<a-tooltip :title="[[ dbInbound.address ]]">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="delete" @click="clearClientIps"></a-icon>
|
<a-icon type="delete" @click="clearClientIps"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -558,74 +542,13 @@
|
||||||
<script>
|
<script>
|
||||||
function refreshIPs(email) {
|
function refreshIPs(email) {
|
||||||
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
||||||
if (!msg.success) {
|
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 {
|
try {
|
||||||
let ips = msg.obj;
|
return JSON.parse(msg.obj).join(', ');
|
||||||
// If msg.obj is a string, try to parse it
|
|
||||||
if (typeof ips === 'string') {
|
|
||||||
try {
|
|
||||||
ips = JSON.parse(ips);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { text: String(ips), array: [String(ips)] };
|
return msg.obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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: [] };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -643,7 +566,6 @@
|
||||||
subLink: '',
|
subLink: '',
|
||||||
subJsonLink: '',
|
subJsonLink: '',
|
||||||
clientIps: '',
|
clientIps: '',
|
||||||
clientIpsArray: [],
|
|
||||||
show(dbInbound, index) {
|
show(dbInbound, index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
|
|
@ -661,9 +583,8 @@
|
||||||
].includes(this.inbound.protocol)
|
].includes(this.inbound.protocol)
|
||||||
) {
|
) {
|
||||||
if (app.ipLimitEnable && this.clientSettings.limitIp) {
|
if (app.ipLimitEnable && this.clientSettings.limitIp) {
|
||||||
refreshIPs(this.clientStats.email).then((result) => {
|
refreshIPs(this.clientStats.email).then((ips) => {
|
||||||
this.clientIps = result.text;
|
this.clientIps = ips;
|
||||||
this.clientIpsArray = result.array;
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -734,35 +655,6 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
copy(content) {
|
||||||
ClipboardManager
|
ClipboardManager
|
||||||
.copyText(content)
|
.copyText(content)
|
||||||
|
|
@ -780,9 +672,8 @@
|
||||||
refreshIPs() {
|
refreshIPs() {
|
||||||
this.refreshing = true;
|
this.refreshing = true;
|
||||||
refreshIPs(this.infoModal.clientStats.email)
|
refreshIPs(this.infoModal.clientStats.email)
|
||||||
.then((result) => {
|
.then((ips) => {
|
||||||
this.infoModal.clientIps = result.text;
|
this.infoModal.clientIps = ips;
|
||||||
this.infoModal.clientIpsArray = result.array;
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
|
|
@ -795,7 +686,6 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.infoModal.clientIps = 'No IP Record';
|
this.infoModal.clientIps = 'No IP Record';
|
||||||
this.infoModal.clientIpsArray = [];
|
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2141,43 +2141,6 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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
|
return InboundClientIps.Ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -3084,41 +3083,9 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
||||||
ips = t.I18nBot("tgbot.noIpRecord")
|
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 := ""
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
||||||
output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps)
|
output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips)
|
||||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
|
|
|
||||||
8
x-ui.sh
8
x-ui.sh
|
|
@ -2062,15 +2062,11 @@ SSH_port_forwarding() {
|
||||||
)
|
)
|
||||||
local server_ip=""
|
local server_ip=""
|
||||||
for ip_address in "${URL_lists[@]}"; do
|
for ip_address in "${URL_lists[@]}"; do
|
||||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||||
local http_code=$(echo "$response" | tail -n1)
|
if [[ -n "${server_ip}" ]]; then
|
||||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
|
||||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
|
||||||
server_ip="${ip_result}"
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
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_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}')
|
local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue