mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-18 14:55:49 +00:00
Merge pull request #52 from hossinasaadi/multi-user-meta-data
Multi user meta data
This commit is contained in:
BIN
bin/geoip.dat
BIN
bin/geoip.dat
Binary file not shown.
22380
bin/geosite.dat
22380
bin/geosite.dat
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"x-ui/config"
|
||||
"x-ui/xray"
|
||||
"x-ui/database/model"
|
||||
)
|
||||
|
||||
@@ -43,6 +44,9 @@ func initSetting() error {
|
||||
func initInboundClientIps() error {
|
||||
return db.AutoMigrate(&model.InboundClientIps{})
|
||||
}
|
||||
func initClientTraffic() error {
|
||||
return db.AutoMigrate(&xray.ClientTraffic{})
|
||||
}
|
||||
|
||||
func InitDB(dbPath string) error {
|
||||
dir := path.Dir(dbPath)
|
||||
@@ -83,7 +87,11 @@ func InitDB(dbPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = initClientTraffic()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ type Inbound struct {
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
|
||||
|
||||
// config part
|
||||
Listen string `json:"listen" form:"listen"`
|
||||
@@ -75,4 +76,6 @@ type Client struct {
|
||||
Email string `json:"email"`
|
||||
LimitIP int `json:"limitIp"`
|
||||
Security string `json:"security"`
|
||||
}
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class DBInbound {
|
||||
this.streamSettings = "";
|
||||
this.tag = "";
|
||||
this.sniffing = "";
|
||||
|
||||
this.clientStats = ""
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
@@ -125,6 +125,7 @@ class DBInbound {
|
||||
if (!ObjectUtil.isEmpty(this.sniffing)) {
|
||||
sniffing = JSON.parse(this.sniffing);
|
||||
}
|
||||
|
||||
const config = {
|
||||
port: this.port,
|
||||
listen: this.listen,
|
||||
@@ -133,6 +134,7 @@ class DBInbound {
|
||||
streamSettings: streamSettings,
|
||||
tag: this.tag,
|
||||
sniffing: sniffing,
|
||||
clientStats: this.clientStats,
|
||||
};
|
||||
return Inbound.fromJson(config);
|
||||
}
|
||||
|
||||
@@ -608,6 +608,7 @@ class Inbound extends XrayCommonClass {
|
||||
streamSettings=new StreamSettings(),
|
||||
tag='',
|
||||
sniffing=new Sniffing(),
|
||||
clientStats='',
|
||||
) {
|
||||
super();
|
||||
this.port = port;
|
||||
@@ -617,6 +618,10 @@ class Inbound extends XrayCommonClass {
|
||||
this.stream = streamSettings;
|
||||
this.tag = tag;
|
||||
this.sniffing = sniffing;
|
||||
this.clientStats = clientStats;
|
||||
}
|
||||
getClientStats() {
|
||||
return this.clientStats;
|
||||
}
|
||||
|
||||
get protocol() {
|
||||
@@ -810,6 +815,21 @@ class Inbound extends XrayCommonClass {
|
||||
return this.stream.grpc.serviceName;
|
||||
}
|
||||
|
||||
isExpiry(index) {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
if(this.settings.vmesses[index]._expiryTime != null)
|
||||
return this.settings.vmesses[index]._expiryTime < new Date().getTime();
|
||||
return false
|
||||
case Protocols.VLESS:
|
||||
if(this.settings.vlesses[index]._expiryTime != null)
|
||||
return this.settings.vlesses[index]._expiryTime < new Date().getTime();
|
||||
return false
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
canEnableTls() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
@@ -1055,6 +1075,7 @@ class Inbound extends XrayCommonClass {
|
||||
StreamSettings.fromJson(json.streamSettings),
|
||||
json.tag,
|
||||
Sniffing.fromJson(json.sniffing),
|
||||
json.clientStats
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1071,6 +1092,7 @@ class Inbound extends XrayCommonClass {
|
||||
streamSettings: streamSettings,
|
||||
tag: this.tag,
|
||||
sniffing: this.sniffing.toJson(),
|
||||
clientStats: this.clientStats
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1157,13 +1179,14 @@ Inbound.VmessSettings = class extends Inbound.Settings {
|
||||
}
|
||||
};
|
||||
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||
constructor(id=RandomUtil.randomUUID(), alterId=0, email='', limitIp=0) {
|
||||
constructor(id=RandomUtil.randomUUID(), alterId=0, email='', limitIp=0, totalGB=0, expiryTime='') {
|
||||
super();
|
||||
this.id = id;
|
||||
this.alterId = alterId;
|
||||
this.email = email;
|
||||
this.limitIp = limitIp;
|
||||
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
@@ -1172,9 +1195,33 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||
json.alterId,
|
||||
json.email,
|
||||
json.limitIp,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
|
||||
);
|
||||
}
|
||||
get _expiryTime() {
|
||||
if (this.expiryTime === 0 || this.expiryTime === "") {
|
||||
return null;
|
||||
}
|
||||
return moment(this.expiryTime);
|
||||
}
|
||||
|
||||
set _expiryTime(t) {
|
||||
if (t == null || t === "") {
|
||||
this.expiryTime = 0;
|
||||
} else {
|
||||
this.expiryTime = t.valueOf();
|
||||
}
|
||||
}
|
||||
get _totalGB() {
|
||||
return toFixed(this.totalGB / ONE_GB, 2);
|
||||
}
|
||||
|
||||
set _totalGB(gb) {
|
||||
this.totalGB = toFixed(gb * ONE_GB, 0);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
@@ -1212,15 +1259,19 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
|
||||
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT, email='', limitIp=0) {
|
||||
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT, email='', limitIp=0, totalGB=0, expiryTime='') {
|
||||
super();
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
this.email = email;
|
||||
this.limitIp = limitIp;
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
@@ -1228,9 +1279,34 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
json.id,
|
||||
json.flow,
|
||||
json.email,
|
||||
json.limitIp
|
||||
json.limitIp,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
get _expiryTime() {
|
||||
if (this.expiryTime === 0 || this.expiryTime === "") {
|
||||
return null;
|
||||
}
|
||||
return moment(this.expiryTime);
|
||||
}
|
||||
|
||||
set _expiryTime(t) {
|
||||
if (t == null || t === "") {
|
||||
this.expiryTime = 0;
|
||||
} else {
|
||||
this.expiryTime = t.valueOf();
|
||||
}
|
||||
}
|
||||
get _totalGB() {
|
||||
return toFixed(this.totalGB / ONE_GB, 2);
|
||||
}
|
||||
|
||||
set _totalGB(gb) {
|
||||
this.totalGB = toFixed(gb * ONE_GB, 0);
|
||||
}
|
||||
};
|
||||
Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
|
||||
constructor(name="", alpn='', path='', dest='', xver=0) {
|
||||
|
||||
@@ -1,87 +1,123 @@
|
||||
{{define "form/vless"}}
|
||||
<a-form layout="inline" v-for="(vless, index) in inbound.settings.vlesses"
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
|
||||
:key="`vless-${index}`">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Email">
|
||||
<a-input v-model.trim="vless.email"></a-input>
|
||||
|
||||
<a-collapse-panel :header="getHeaderText(vless.email)">
|
||||
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Email">
|
||||
<a-input v-model.trim="vless.email"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
IP Count Limit
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
disable inbound if more than entered count (0 for disable limit ip)
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-input type="number" v-model.number="vless.limitIp" min="0" ></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit">
|
||||
<span slot="label">
|
||||
IP log
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-form layout="block">
|
||||
|
||||
<a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
|
||||
</a-textarea>
|
||||
|
||||
<a-button type="danger" @click="clearDBClientIps(vless.email,$event)" >clear log</a-button>
|
||||
</a-form>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="vless.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="vless.flow" style="width: 150px">
|
||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
IP Count Limit
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
disable inbound if more than entered count (0 for disable limit ip)
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-input type="number" v-model.number="vless.limitIp" min="0" ></a-input>
|
||||
<a-input-number v-model="vless._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
IP log
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-form layout="block">
|
||||
|
||||
<a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
|
||||
</a-textarea>
|
||||
|
||||
<a-button type="danger" @click="clearDBClientIps(vless.email,$event)" >clear log</a-button>
|
||||
</a-form>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="vless._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="vless.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="vless.flow" style="width: 150px">
|
||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form layout="inline">
|
||||
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
|
||||
<a-form v-if="vless._totalGB > 0">
|
||||
<a-tag color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
|
||||
</a-form>
|
||||
</a-form>
|
||||
|
||||
<!--Add Svg Icon-->
|
||||
<svg
|
||||
|
||||
@click="addClient(inbound.protocol,vless, inbound.settings.vlesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
@click="addClient(inbound.protocol,vless, inbound.settings.vlesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!--Remove Svg Icon-->
|
||||
<svg
|
||||
v-show="inbound.settings.vlesses.length > 1"
|
||||
@click="removeClient(index, inbound.settings.vlesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
<a-divider style="height: 2px; background-color: #7e7e7e" />
|
||||
</a-form>
|
||||
<!--Remove Svg Icon-->
|
||||
<svg
|
||||
v-show="inbound.settings.vlesses.length > 1"
|
||||
@click="removeClient(index, inbound.settings.vlesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
</a-form>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="fallbacks">
|
||||
|
||||
@@ -1,81 +1,122 @@
|
||||
{{define "form/vmess"}}
|
||||
<a-form layout="inline" v-for="(vmess, index) in inbound.settings.vmesses"
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
|
||||
:key="`vmess-${index}`">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Email">
|
||||
<a-input v-model.trim="vmess.email"></a-input>
|
||||
<a-collapse-panel :header="getHeaderText(vmess.email)">
|
||||
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Email">
|
||||
<a-input v-model.trim="vmess.email"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
IP Count Limit
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
disable inbound if more than entered count (0 for disable limit ip)
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-input type="number" v-model.number="vmess.limitIp" min="0" ></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
|
||||
<span slot="label">
|
||||
IP Log
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
|
||||
</a-textarea>
|
||||
|
||||
<a-button type="danger" @click="clearDBClientIps(vmess.email,$event)" >clear log</a-button>
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="vmess.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
||||
<a-input type="number" v-model.number="vmess.alterId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
IP Count Limit
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
disable inbound if more than entered count (0 for disable limit ip)
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-input type="number" v-model.number="vmess.limitIp" min="0" ></a-input>
|
||||
<a-input-number v-model="vmess._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
IP Log
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
|
||||
</a-textarea>
|
||||
|
||||
<a-button type="danger" @click="clearDBClientIps(vmess.email,$event)" >clear log</a-button>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="vmess._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
<a-form layout="inline">
|
||||
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
|
||||
<a-form v-if="vmess._totalGB > 0">
|
||||
<a-tag color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
|
||||
</a-form>
|
||||
</a-form>
|
||||
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="vmess.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
||||
<a-input type="number" v-model.number="vmess.alterId"></a-input>
|
||||
</a-form-item>
|
||||
<!--Add Svg Icon-->
|
||||
<!--Add Svg Icon-->
|
||||
<svg
|
||||
|
||||
@click="addClient(inbound.protocol,vmess, inbound.settings.vmesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
@click="addClient(inbound.protocol,vmess, inbound.settings.vmesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!--Remove Svg Icon-->
|
||||
<svg
|
||||
v-show="inbound.settings.vmesses.length > 1"
|
||||
@click="removeClient(index, inbound.settings.vmesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
<!-- <a-divider style="height: 2px; background-color: #7e7e7e" /> -->
|
||||
|
||||
<!--Remove Svg Icon-->
|
||||
<svg
|
||||
v-show="inbound.settings.vmesses.length > 1"
|
||||
@click="removeClient(index, inbound.settings.vmesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
<a-divider style="height: 2px; background-color: #7e7e7e" />
|
||||
</a-collapse-panel>
|
||||
|
||||
</a-collapse>
|
||||
|
||||
|
||||
</a-form>
|
||||
</a-form>
|
||||
|
||||
@@ -112,6 +112,57 @@
|
||||
}
|
||||
event.target.value = ""
|
||||
},
|
||||
isExpiry(index) {
|
||||
return this.inbound.isExpiry(index)
|
||||
},
|
||||
getUpStats(email) {
|
||||
clientStats = this.inbound.clientStats
|
||||
if(clientStats.length > 0)
|
||||
{
|
||||
for (const key in clientStats) {
|
||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||
if(clientStats[key]['email'] == email)
|
||||
return clientStats[key]['up']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
getDownStats(email) {
|
||||
clientStats = this.inbound.clientStats
|
||||
if(clientStats.length > 0)
|
||||
{
|
||||
for (const key in clientStats) {
|
||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||
if(clientStats[key]['email'] == email)
|
||||
return clientStats[key]['down']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isClientEnable(email) {
|
||||
clientStats = this.inbound.clientStats
|
||||
if(clientStats.length > 0)
|
||||
{
|
||||
for (const key in clientStats) {
|
||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||
if(clientStats[key]['email'] == email)
|
||||
return clientStats[key]['enable']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getHeaderText(email) {
|
||||
if(email == "")
|
||||
return "Add Client"
|
||||
|
||||
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,7 +15,15 @@ func NewCheckInboundJob() *CheckInboundJob {
|
||||
}
|
||||
|
||||
func (j *CheckInboundJob) Run() {
|
||||
count, err := j.inboundService.DisableInvalidInbounds()
|
||||
count, err := j.inboundService.DisableInvalidClients()
|
||||
if err != nil {
|
||||
logger.Warning("disable invalid Client err:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("disabled %v Client", count)
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
|
||||
count, err = j.inboundService.DisableInvalidInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("disable invalid inbounds err:", err)
|
||||
} else if count > 0 {
|
||||
|
||||
@@ -18,6 +18,19 @@ func (j *XrayTrafficJob) Run() {
|
||||
if !j.xrayService.IsXrayRunning() {
|
||||
return
|
||||
}
|
||||
|
||||
// get Client Traffic
|
||||
|
||||
clientTraffics, err := j.xrayService.GetXrayClientTraffic()
|
||||
if err != nil {
|
||||
logger.Warning("get xray client traffic failed:", err)
|
||||
return
|
||||
}
|
||||
err = j.inboundService.AddClientTraffic(clientTraffics)
|
||||
if err != nil {
|
||||
logger.Warning("add client traffic failed:", err)
|
||||
}
|
||||
|
||||
traffics, err := j.xrayService.GetXrayTraffic()
|
||||
if err != nil {
|
||||
logger.Warning("get xray traffic failed:", err)
|
||||
@@ -27,4 +40,7 @@ func (j *XrayTrafficJob) Run() {
|
||||
if err != nil {
|
||||
logger.Warning("add traffic failed:", err)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
"x-ui/database"
|
||||
"encoding/json"
|
||||
"x-ui/database/model"
|
||||
"x-ui/util/common"
|
||||
"x-ui/xray"
|
||||
@@ -17,7 +18,7 @@ type InboundService struct {
|
||||
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Where("user_id = ?", userId).Find(&inbounds).Error
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
@@ -27,7 +28,7 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
||||
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Find(&inbounds).Error
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
@@ -57,8 +58,12 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound,erro
|
||||
return inbound, common.NewError("端口已存在:", inbound.Port)
|
||||
}
|
||||
db := database.GetDB()
|
||||
|
||||
return inbound, db.Save(inbound).Error
|
||||
|
||||
err = db.Save(inbound).Error
|
||||
if err == nil {
|
||||
s.UpdateClientStat(inbound.Id,inbound.Settings)
|
||||
}
|
||||
return inbound, err
|
||||
}
|
||||
|
||||
func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
|
||||
@@ -135,6 +140,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
oldInbound.Sniffing = inbound.Sniffing
|
||||
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
|
||||
s.UpdateClientStat(inbound.Id,inbound.Settings)
|
||||
db := database.GetDB()
|
||||
return inbound, db.Save(oldInbound).Error
|
||||
}
|
||||
@@ -166,6 +172,54 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
|
||||
}
|
||||
return
|
||||
}
|
||||
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
|
||||
if len(traffics) == 0 {
|
||||
return nil
|
||||
}
|
||||
db := database.GetDB()
|
||||
db = db.Model(xray.ClientTraffic{})
|
||||
tx := db.Begin()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
for _, traffic := range traffics {
|
||||
inbound := &model.Inbound{}
|
||||
|
||||
err := db.Model(model.Inbound{}).Where("settings like ?", "%" + traffic.Email + "%").First(inbound).Error
|
||||
traffic.InboundId = inbound.Id
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// get settings clients
|
||||
settings := map[string][]model.Client{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
clients := settings["clients"]
|
||||
for _, client := range clients {
|
||||
if traffic.Email == client.Email {
|
||||
traffic.ExpiryTime = client.ExpiryTime
|
||||
traffic.Total = client.TotalGB
|
||||
}
|
||||
}
|
||||
if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email).
|
||||
UpdateColumn("enable", true).
|
||||
UpdateColumn("expiry_time", traffic.ExpiryTime).
|
||||
UpdateColumn("total",traffic.Total).
|
||||
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
|
||||
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 {
|
||||
err = tx.Create(traffic).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
||||
db := database.GetDB()
|
||||
@@ -177,6 +231,46 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
||||
count := result.RowsAffected
|
||||
return count, err
|
||||
}
|
||||
func (s *InboundService) DisableInvalidClients() (int64, error) {
|
||||
db := database.GetDB()
|
||||
now := time.Now().Unix() * 1000
|
||||
result := db.Model(xray.ClientTraffic{}).
|
||||
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
|
||||
Update("enable", false)
|
||||
err := result.Error
|
||||
count := result.RowsAffected
|
||||
return count, err
|
||||
}
|
||||
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) (error) {
|
||||
db := database.GetDB()
|
||||
|
||||
// get settings clients
|
||||
settings := map[string][]model.Client{}
|
||||
json.Unmarshal([]byte(inboundSettings), &settings)
|
||||
clients := settings["clients"]
|
||||
for _, client := range clients {
|
||||
result := db.Model(xray.ClientTraffic{}).
|
||||
Where("inbound_id = ? and email = ?", inboundId, client.Email).
|
||||
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime})
|
||||
if result.RowsAffected == 0 {
|
||||
clientTraffic := xray.ClientTraffic{}
|
||||
clientTraffic.InboundId = inboundId
|
||||
clientTraffic.Email = client.Email
|
||||
clientTraffic.Total = client.TotalGB
|
||||
clientTraffic.ExpiryTime = client.ExpiryTime
|
||||
clientTraffic.Enable = true
|
||||
clientTraffic.Up = 0
|
||||
clientTraffic.Down = 0
|
||||
db.Create(&clientTraffic)
|
||||
}
|
||||
err := result.Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
|
||||
db := database.GetDB()
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"sync"
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
@@ -51,6 +50,9 @@ func (s *XrayService) GetXrayVersion() string {
|
||||
}
|
||||
return p.GetVersion()
|
||||
}
|
||||
func RemoveIndex(s []interface{}, index int) []interface{} {
|
||||
return append(s[:index], s[index+1:]...)
|
||||
}
|
||||
|
||||
func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
templateConfig, err := s.settingService.GetXrayConfigTemplate()
|
||||
@@ -64,6 +66,8 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.inboundService.DisableInvalidClients()
|
||||
|
||||
inbounds, err := s.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -72,6 +76,40 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
if !inbound.Enable {
|
||||
continue
|
||||
}
|
||||
// get settings clients
|
||||
settings := map[string]interface{}{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
clients := settings["clients"].([]interface{})
|
||||
|
||||
|
||||
|
||||
// check users active or not
|
||||
|
||||
clientStats := inbound.ClientStats
|
||||
for _, clientTraffic := range clientStats {
|
||||
|
||||
for index, client := range clients {
|
||||
c := client.(map[string]interface{})
|
||||
if c["email"] == clientTraffic.Email {
|
||||
if ! clientTraffic.Enable {
|
||||
clients = RemoveIndex(clients,index)
|
||||
logger.Info("Remove Inbound User",c["email"] ,"due the expire or traffic limit")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
settings["clients"] = clients
|
||||
modifiedSettings, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
|
||||
inboundConfig := inbound.GenXrayInboundConfig()
|
||||
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
|
||||
}
|
||||
@@ -84,6 +122,12 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) {
|
||||
}
|
||||
return p.GetTraffic(true)
|
||||
}
|
||||
func (s *XrayService) GetXrayClientTraffic() ([]*xray.ClientTraffic, error) {
|
||||
if !s.IsXrayRunning() {
|
||||
return nil, errors.New("xray is not running")
|
||||
}
|
||||
return p.GetClientTraffic(false)
|
||||
}
|
||||
|
||||
func (s *XrayService) RestartXray(isForce bool) error {
|
||||
lock.Lock()
|
||||
|
||||
12
xray/client_traffic.go
Normal file
12
xray/client_traffic.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package xray
|
||||
|
||||
type ClientTraffic struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
InboundId int `json:"inboundId" form:"inboundId"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
Email string `json:"email" form:"email" gorm:"unique"`
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
)
|
||||
|
||||
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
|
||||
func GetBinaryName() string {
|
||||
return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
|
||||
@@ -253,6 +254,9 @@ func (p *process) GetTraffic(reset bool) ([]*Traffic, error) {
|
||||
traffics := make([]*Traffic, 0)
|
||||
for _, stat := range resp.GetStat() {
|
||||
matchs := trafficRegex.FindStringSubmatch(stat.Name)
|
||||
if len(matchs) < 3 {
|
||||
continue
|
||||
}
|
||||
isInbound := matchs[1] == "inbound"
|
||||
tag := matchs[2]
|
||||
isDown := matchs[3] == "downlink"
|
||||
@@ -277,3 +281,53 @@ func (p *process) GetTraffic(reset bool) ([]*Traffic, error) {
|
||||
|
||||
return traffics, nil
|
||||
}
|
||||
func (p *process) GetClientTraffic(reset bool) ([]*ClientTraffic, error) {
|
||||
if p.apiPort == 0 {
|
||||
return nil, common.NewError("xray api port wrong:", p.apiPort)
|
||||
}
|
||||
conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := statsservice.NewStatsServiceClient(conn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
request := &statsservice.QueryStatsRequest{
|
||||
Reset_: reset,
|
||||
}
|
||||
resp, err := client.QueryStats(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
emailTrafficMap := map[string]*ClientTraffic{}
|
||||
traffics := make([]*ClientTraffic, 0)
|
||||
for _, stat := range resp.GetStat() {
|
||||
matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name)
|
||||
if len(matchs) < 3 {
|
||||
continue
|
||||
}
|
||||
isUser := matchs[1] == "user"
|
||||
email := matchs[2]
|
||||
isDown := matchs[3] == "downlink"
|
||||
if ! isUser {
|
||||
continue
|
||||
}
|
||||
traffic, ok := emailTrafficMap[email]
|
||||
if !ok {
|
||||
traffic = &ClientTraffic{
|
||||
Email: email,
|
||||
}
|
||||
emailTrafficMap[email] = traffic
|
||||
traffics = append(traffics, traffic)
|
||||
}
|
||||
if isDown {
|
||||
traffic.Down = stat.Value
|
||||
} else {
|
||||
traffic.Up = stat.Value
|
||||
}
|
||||
}
|
||||
|
||||
return traffics, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user