Merge pull request #52 from hossinasaadi/multi-user-meta-data

Multi user meta data
This commit is contained in:
Hossin Asaadi
2022-11-15 16:14:56 +03:30
committed by GitHub
17 changed files with 14183 additions and 8894 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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">

View File

@@ -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>

View File

@@ -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')
},
},
});

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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
View 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"`
}

View File

@@ -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
}