mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-08 14:36:13 +00:00
fix(security): sanitize remote IP headers and escape log viewer output
#4135
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
@@ -14,18 +16,58 @@ import (
|
|||||||
|
|
||||||
// getRemoteIp extracts the real IP address from the request headers or remote address.
|
// getRemoteIp extracts the real IP address from the request headers or remote address.
|
||||||
func getRemoteIp(c *gin.Context) string {
|
func getRemoteIp(c *gin.Context) string {
|
||||||
value := c.GetHeader("X-Real-IP")
|
if ip, ok := extractTrustedIP(c.GetHeader("X-Real-IP")); ok {
|
||||||
if value != "" {
|
return ip
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
value = c.GetHeader("X-Forwarded-For")
|
|
||||||
if value != "" {
|
if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
|
||||||
ips := strings.Split(value, ",")
|
for _, part := range strings.Split(xff, ",") {
|
||||||
return ips[0]
|
if ip, ok := extractTrustedIP(part); ok {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
addr := c.Request.RemoteAddr
|
|
||||||
ip, _, _ := net.SplitHostPort(addr)
|
if ip, ok := extractTrustedIP(c.Request.RemoteAddr); ok {
|
||||||
return ip
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTrustedIP(value string) (string, bool) {
|
||||||
|
candidate := strings.TrimSpace(value)
|
||||||
|
if candidate == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip, ok := parseIPCandidate(candidate); ok {
|
||||||
|
return ip.String(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if host, _, err := net.SplitHostPort(candidate); err == nil {
|
||||||
|
if ip, ok := parseIPCandidate(host); ok {
|
||||||
|
return ip.String(), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Count(candidate, ":") == 1 {
|
||||||
|
if host, _, err := net.SplitHostPort(fmt.Sprintf("[%s]", candidate)); err == nil {
|
||||||
|
if ip, ok := parseIPCandidate(host); ok {
|
||||||
|
return ip.String(), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIPCandidate(value string) (netip.Addr, bool) {
|
||||||
|
ip, err := netip.ParseAddr(strings.TrimSpace(value))
|
||||||
|
if err != nil {
|
||||||
|
return netip.Addr{}, false
|
||||||
|
}
|
||||||
|
return ip.Unmap(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// jsonMsg sends a JSON response with a message and error status.
|
// jsonMsg sends a JSON response with a message and error status.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `{{template "component/customStatistic"}}`,
|
template: `{{template "component/customStatistic" .}}`,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
template: `{{template "component/persianDatepickerTemplate"}}`,
|
template: `{{template "component/persianDatepickerTemplate" .}}`,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
date: '',
|
date: '',
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `{{template "component/sidebar/content"}}`,
|
template: `{{template "component/sidebar/content" .}}`,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
Vue.component('a-table-sort-trigger', {
|
Vue.component('a-table-sort-trigger', {
|
||||||
template: `{{template "component/sortableTableTrigger"}}`,
|
template: `{{template "component/sortableTableTrigger" .}}`,
|
||||||
props: {
|
props: {
|
||||||
'item-index': {
|
'item-index': {
|
||||||
type: undefined,
|
type: undefined,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
}
|
}
|
||||||
const themeSwitcher = createThemeSwitcher();
|
const themeSwitcher = createThemeSwitcher();
|
||||||
Vue.component('a-theme-switch', {
|
Vue.component('a-theme-switch', {
|
||||||
template: `{{template "component/themeSwitchTemplate"}}`,
|
template: `{{template "component/themeSwitchTemplate" .}}`,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
themeSwitcher
|
themeSwitcher
|
||||||
}),
|
}),
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
Vue.component('a-theme-switch-login', {
|
Vue.component('a-theme-switch-login', {
|
||||||
template: `{{template "component/themeSwitchTemplateLogin"}}`,
|
template: `{{template "component/themeSwitchTemplateLogin" .}}`,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
themeSwitcher
|
themeSwitcher
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -102,69 +102,69 @@
|
|||||||
|
|
||||||
<!-- vmess settings -->
|
<!-- vmess settings -->
|
||||||
<template v-if="inbound.protocol === Protocols.VMESS">
|
<template v-if="inbound.protocol === Protocols.VMESS">
|
||||||
{{template "form/vmess"}}
|
{{template "form/vmess" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- vless settings -->
|
<!-- vless settings -->
|
||||||
<template v-if="inbound.protocol === Protocols.VLESS">
|
<template v-if="inbound.protocol === Protocols.VLESS">
|
||||||
{{template "form/vless"}}
|
{{template "form/vless" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- trojan settings -->
|
<!-- trojan settings -->
|
||||||
<template v-if="inbound.protocol === Protocols.TROJAN">
|
<template v-if="inbound.protocol === Protocols.TROJAN">
|
||||||
{{template "form/trojan"}}
|
{{template "form/trojan" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- shadowsocks -->
|
<!-- shadowsocks -->
|
||||||
<template v-if="inbound.protocol === Protocols.SHADOWSOCKS">
|
<template v-if="inbound.protocol === Protocols.SHADOWSOCKS">
|
||||||
{{template "form/shadowsocks"}}
|
{{template "form/shadowsocks" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- tunnel -->
|
<!-- tunnel -->
|
||||||
<template v-if="inbound.protocol === Protocols.TUNNEL">
|
<template v-if="inbound.protocol === Protocols.TUNNEL">
|
||||||
{{template "form/tunnel"}}
|
{{template "form/tunnel" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- mixed -->
|
<!-- mixed -->
|
||||||
<template v-if="inbound.protocol === Protocols.MIXED">
|
<template v-if="inbound.protocol === Protocols.MIXED">
|
||||||
{{template "form/mixed"}}
|
{{template "form/mixed" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- http -->
|
<!-- http -->
|
||||||
<template v-if="inbound.protocol === Protocols.HTTP">
|
<template v-if="inbound.protocol === Protocols.HTTP">
|
||||||
{{template "form/http"}}
|
{{template "form/http" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- wireguard -->
|
<!-- wireguard -->
|
||||||
<template v-if="inbound.protocol === Protocols.WIREGUARD">
|
<template v-if="inbound.protocol === Protocols.WIREGUARD">
|
||||||
{{template "form/wireguard"}}
|
{{template "form/wireguard" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- tun -->
|
<!-- tun -->
|
||||||
<template v-if="inbound.protocol === Protocols.TUN">
|
<template v-if="inbound.protocol === Protocols.TUN">
|
||||||
{{template "form/tun"}}
|
{{template "form/tun" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- hysteria -->
|
<!-- hysteria -->
|
||||||
<template v-if="inbound.protocol === Protocols.HYSTERIA">
|
<template v-if="inbound.protocol === Protocols.HYSTERIA">
|
||||||
{{template "form/hysteria"}}
|
{{template "form/hysteria" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- stream settings -->
|
<!-- stream settings -->
|
||||||
<template v-if="inbound.canEnableStream()">
|
<template v-if="inbound.canEnableStream()">
|
||||||
{{template "form/streamSettings"}}
|
{{template "form/streamSettings" .}}
|
||||||
{{template "form/externalProxy" }}
|
{{template "form/externalProxy" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- tls settings -->
|
<!-- tls settings -->
|
||||||
<template v-if="inbound.canEnableTls()">
|
<template v-if="inbound.canEnableTls()">
|
||||||
{{template "form/tlsSettings"}}
|
{{template "form/tlsSettings" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- sniffing -->
|
<!-- sniffing -->
|
||||||
<a-collapse>
|
<a-collapse>
|
||||||
<a-collapse-panel header='Sniffing'>
|
<a-collapse-panel header='Sniffing'>
|
||||||
{{template "form/sniffing"}}
|
{{template "form/sniffing" .}}
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,6 @@
|
|||||||
</a-form>
|
</a-form>
|
||||||
<!-- sockopt -->
|
<!-- sockopt -->
|
||||||
<template>
|
<template>
|
||||||
{{template "form/streamSockopt"}}
|
{{template "form/streamSockopt" .}}
|
||||||
</template>
|
</template>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "form/hysteria"}}
|
{{define "form/hysteria"}}
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.hysterias.slice(0,1)" v-if="!isEdit">
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.hysterias.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||||
{{template "form/client"}}
|
{{template "form/client" .}}
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<template v-if="inbound.isSSMultiUser">
|
<template v-if="inbound.isSSMultiUser">
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||||
{{template "form/client"}}
|
{{template "form/client" .}}
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "form/trojan"}}
|
{{define "form/trojan"}}
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||||
{{template "form/client"}}
|
{{template "form/client" .}}
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "form/vless"}}
|
{{define "form/vless"}}
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||||
{{template "form/client"}}
|
{{template "form/client" .}}
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{define "form/vmess"}}
|
{{define "form/vmess"}}
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||||
{{template "form/client"}}
|
{{template "form/client" .}}
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
|
|||||||
@@ -17,42 +17,42 @@
|
|||||||
|
|
||||||
<!-- tcp -->
|
<!-- tcp -->
|
||||||
<template v-if="inbound.stream.network === 'tcp'">
|
<template v-if="inbound.stream.network === 'tcp'">
|
||||||
{{template "form/streamTCP"}}
|
{{template "form/streamTCP" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- kcp -->
|
<!-- kcp -->
|
||||||
<template v-if="inbound.stream.network === 'kcp'">
|
<template v-if="inbound.stream.network === 'kcp'">
|
||||||
{{template "form/streamKCP"}}
|
{{template "form/streamKCP" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ws -->
|
<!-- ws -->
|
||||||
<template v-if="inbound.stream.network === 'ws'">
|
<template v-if="inbound.stream.network === 'ws'">
|
||||||
{{template "form/streamWS"}}
|
{{template "form/streamWS" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- grpc -->
|
<!-- grpc -->
|
||||||
<template v-if="inbound.stream.network === 'grpc'">
|
<template v-if="inbound.stream.network === 'grpc'">
|
||||||
{{template "form/streamGRPC"}}
|
{{template "form/streamGRPC" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- hysteria -->
|
<!-- hysteria -->
|
||||||
<template v-if="inbound.stream.network === 'hysteria'">
|
<template v-if="inbound.stream.network === 'hysteria'">
|
||||||
{{template "form/streamHysteria"}}
|
{{template "form/streamHysteria" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- httpupgrade -->
|
<!-- httpupgrade -->
|
||||||
<template v-if="inbound.stream.network === 'httpupgrade'">
|
<template v-if="inbound.stream.network === 'httpupgrade'">
|
||||||
{{template "form/streamHTTPUpgrade"}}
|
{{template "form/streamHTTPUpgrade" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- xhttp -->
|
<!-- xhttp -->
|
||||||
<template v-if="inbound.stream.network === 'xhttp'">
|
<template v-if="inbound.stream.network === 'xhttp'">
|
||||||
{{template "form/streamXHTTP"}}
|
{{template "form/streamXHTTP" .}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- sockopt -->
|
<!-- sockopt -->
|
||||||
<template> {{template "form/streamSockopt"}} </template>
|
<template> {{template "form/streamSockopt" .}} </template>
|
||||||
|
|
||||||
<!-- finalmask -->
|
<!-- finalmask -->
|
||||||
<template> {{template "form/streamFinalMask"}} </template>
|
<template> {{template "form/streamFinalMask" .}} </template>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
|
|
||||||
<!-- reality settings -->
|
<!-- reality settings -->
|
||||||
<template v-if="inbound.stream.isReality">
|
<template v-if="inbound.stream.isReality">
|
||||||
{{template "form/realitySettings"}}
|
{{template "form/realitySettings" .}}
|
||||||
</template>
|
</template>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -645,7 +645,7 @@
|
|||||||
<a-table :row-key="client => client.id" :columns="isMobile ? innerMobileColumns : innerColumns"
|
<a-table :row-key="client => client.id" :columns="isMobile ? innerMobileColumns : innerColumns"
|
||||||
:data-source="getInboundClients(record)" :pagination=pagination(getInboundClients(record))
|
:data-source="getInboundClients(record)" :pagination=pagination(getInboundClients(record))
|
||||||
:style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
|
:style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
|
||||||
{{template "component/aClientTable"}}
|
{{template "component/aClientTable" .}}
|
||||||
</a-table>
|
</a-table>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
@@ -668,13 +668,13 @@
|
|||||||
{{template "component/aThemeSwitch" .}}
|
{{template "component/aThemeSwitch" .}}
|
||||||
{{template "component/aCustomStatistic" .}}
|
{{template "component/aCustomStatistic" .}}
|
||||||
{{template "component/aPersianDatepicker" .}}
|
{{template "component/aPersianDatepicker" .}}
|
||||||
{{template "modals/inboundModal"}}
|
{{template "modals/inboundModal" .}}
|
||||||
{{template "modals/promptModal"}}
|
{{template "modals/promptModal" .}}
|
||||||
{{template "modals/qrcodeModal"}}
|
{{template "modals/qrcodeModal" .}}
|
||||||
{{template "modals/textModal"}}
|
{{template "modals/textModal" .}}
|
||||||
{{template "modals/inboundInfoModal"}}
|
{{template "modals/inboundInfoModal" .}}
|
||||||
{{template "modals/clientsModal"}}
|
{{template "modals/clientsModal" .}}
|
||||||
{{template "modals/clientsBulkModal"}}
|
{{template "modals/clientsBulkModal" .}}
|
||||||
<a-modal id="copy-clients-modal" :title="copyClientsModal.title" :visible="copyClientsModal.visible"
|
<a-modal id="copy-clients-modal" :title="copyClientsModal.title" :visible="copyClientsModal.visible"
|
||||||
:confirm-loading="copyClientsModal.confirmLoading" ok-text='{{ i18n "pages.client.copySelected" }}'
|
:confirm-loading="copyClientsModal.confirmLoading" ok-text='{{ i18n "pages.client.copySelected" }}'
|
||||||
cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme" :closable="true" :mask-closable="false"
|
cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme" :closable="true" :mask-closable="false"
|
||||||
|
|||||||
@@ -564,7 +564,7 @@
|
|||||||
{{template "component/aSidebar" .}}
|
{{template "component/aSidebar" .}}
|
||||||
{{template "component/aThemeSwitch" .}}
|
{{template "component/aThemeSwitch" .}}
|
||||||
{{template "component/aCustomStatistic" .}}
|
{{template "component/aCustomStatistic" .}}
|
||||||
{{template "modals/textModal"}}
|
{{template "modals/textModal" .}}
|
||||||
<script>
|
<script>
|
||||||
// Tiny Sparkline component using an inline SVG polyline
|
// Tiny Sparkline component using an inline SVG polyline
|
||||||
Vue.component('sparkline', {
|
Vue.component('sparkline', {
|
||||||
@@ -963,6 +963,18 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const escapeHtml = (value) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
};
|
||||||
|
|
||||||
const logModal = {
|
const logModal = {
|
||||||
visible: false,
|
visible: false,
|
||||||
logs: [],
|
logs: [],
|
||||||
@@ -986,24 +998,28 @@
|
|||||||
if (index > 0) formattedLogs += '<br>';
|
if (index > 0) formattedLogs += '<br>';
|
||||||
|
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
const d = parts[0];
|
const d = escapeHtml(parts[0]);
|
||||||
const t = parts[1];
|
const t = escapeHtml(parts[1]);
|
||||||
const level = parts[2];
|
const levelRaw = parts[2];
|
||||||
const levelIndex = levels.indexOf(level, levels) || 5;
|
const level = escapeHtml(levelRaw);
|
||||||
|
const idx = levels.indexOf(levelRaw);
|
||||||
|
const levelIndex = idx >= 0 ? idx : 5;
|
||||||
|
|
||||||
//formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
|
//formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
|
||||||
formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
|
formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
|
||||||
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
|
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
|
||||||
} else {
|
} else {
|
||||||
const levelIndex = levels.indexOf(data, levels) || 5;
|
const idx = levels.indexOf(data);
|
||||||
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
|
const levelIndex = idx >= 0 ? idx : 5;
|
||||||
|
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${escapeHtml(data)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
if (message.startsWith("XRAY:"))
|
if (message.startsWith("XRAY:")) {
|
||||||
message = "<b>XRAY: </b>" + message.substring(5);
|
message = "<b>XRAY: </b>" + escapeHtml(message.substring(5));
|
||||||
else
|
} else {
|
||||||
message = "<b>X-UI: </b>" + message;
|
message = "<b>X-UI: </b>" + escapeHtml(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedLogs += message ? ' - ' + message : '';
|
formattedLogs += message ? ' - ' + message : '';
|
||||||
@@ -1063,16 +1079,16 @@
|
|||||||
|
|
||||||
let text = ``;
|
let text = ``;
|
||||||
if (log.Email !== "") {
|
if (log.Email !== "") {
|
||||||
text = `<td>${log.Email}</td>`;
|
text = `<td>${escapeHtml(log.Email)}</td>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedLogs += `
|
formattedLogs += `
|
||||||
<tr ${outboundColor}>
|
<tr ${outboundColor}>
|
||||||
<td><b>${IntlUtil.formatDate(log.DateTime)}</b></td>
|
<td><b>${escapeHtml(IntlUtil.formatDate(log.DateTime))}</b></td>
|
||||||
<td>${log.FromAddress}</td>
|
<td>${escapeHtml(log.FromAddress)}</td>
|
||||||
<td>${log.ToAddress}</td>
|
<td>${escapeHtml(log.ToAddress)}</td>
|
||||||
<td>${log.Inbound}</td>
|
<td>${escapeHtml(log.Inbound)}</td>
|
||||||
<td>${log.Outbound}</td>
|
<td>${escapeHtml(log.Outbound)}</td>
|
||||||
${text}
|
${text}
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -150,7 +150,11 @@
|
|||||||
},
|
},
|
||||||
initHeadline() {
|
initHeadline() {
|
||||||
const animationDelay = 2000;
|
const animationDelay = 2000;
|
||||||
const headlines = this.$el.querySelectorAll('.headline');
|
const rootEl = this.$el instanceof Element ? this.$el : document.getElementById('app');
|
||||||
|
if (!rootEl || typeof rootEl.querySelectorAll !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headlines = rootEl.querySelectorAll('.headline');
|
||||||
headlines.forEach((headline) => {
|
headlines.forEach((headline) => {
|
||||||
const first = headline.querySelector('.is-visible');
|
const first = headline.querySelector('.is-visible');
|
||||||
if (!first) return;
|
if (!first) return;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
:style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account
|
:style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account
|
||||||
is (Expired|Traffic Ended) And Disabled</a-tag>
|
is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||||
</template>
|
</template>
|
||||||
{{template "form/client"}}
|
{{template "form/client" .}}
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
const clientModal = {
|
const clientModal = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
|
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
|
||||||
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||||
{{template "form/inbound"}}
|
{{template "form/inbound" .}}
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
// Make inModal globally available to ensure it works with any base path
|
// Make inModal globally available to ensure it works with any base path
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
|
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
:ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }"
|
:ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }"
|
||||||
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||||
{{template "form/outbound"}}
|
{{template "form/outbound" .}}
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
const outModal = {
|
const outModal = {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
{{template "component/aSidebar" .}}
|
{{template "component/aSidebar" .}}
|
||||||
{{template "component/aThemeSwitch" .}}
|
{{template "component/aThemeSwitch" .}}
|
||||||
{{template "component/aSettingListItem" .}}
|
{{template "component/aSettingListItem" .}}
|
||||||
{{template "modals/twoFactorModal"}}
|
{{template "modals/twoFactorModal" .}}
|
||||||
<script>
|
<script>
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
|
|||||||
@@ -133,15 +133,15 @@
|
|||||||
{{template "component/aThemeSwitch" .}}
|
{{template "component/aThemeSwitch" .}}
|
||||||
{{template "component/aTableSortable" .}}
|
{{template "component/aTableSortable" .}}
|
||||||
{{template "component/aSettingListItem" .}}
|
{{template "component/aSettingListItem" .}}
|
||||||
{{template "modals/ruleModal"}}
|
{{template "modals/ruleModal" .}}
|
||||||
{{template "modals/outModal"}}
|
{{template "modals/outModal" .}}
|
||||||
{{template "modals/reverseModal"}}
|
{{template "modals/reverseModal" .}}
|
||||||
{{template "modals/balancerModal"}}
|
{{template "modals/balancerModal" .}}
|
||||||
{{template "modals/dnsModal"}}
|
{{template "modals/dnsModal" .}}
|
||||||
{{template "modals/dnsPresetsModal"}}
|
{{template "modals/dnsPresetsModal" .}}
|
||||||
{{template "modals/fakednsModal"}}
|
{{template "modals/fakednsModal" .}}
|
||||||
{{template "modals/warpModal"}}
|
{{template "modals/warpModal" .}}
|
||||||
{{template "modals/nordModal"}}
|
{{template "modals/nordModal" .}}
|
||||||
<script>
|
<script>
|
||||||
const rulesColumns = [{
|
const rulesColumns = [{
|
||||||
title: "#",
|
title: "#",
|
||||||
|
|||||||
Reference in New Issue
Block a user