[feature] add multi domain tls (CDN ready)

This commit is contained in:
Alireza Ahmadi
2023-05-20 21:30:17 +02:00
parent 2b8c913be9
commit 40e4145263
8 changed files with 139 additions and 62 deletions

View File

@@ -92,6 +92,7 @@ docker build -t x-ui .
- Search within all inbounds and clients - Search within all inbounds and clients
- Support Dark/Light theme UI - Support Dark/Light theme UI
- Support multi-user multi-protocol, web page visualization operation - Support multi-user multi-protocol, web page visualization operation
- Support multi-domain configuration and multi-certificate inbounds
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http - Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
- Support for configuring more transport configurations - Support for configuring more transport configurations
- Traffic statistics, limit traffic, limit expiration time - Traffic statistics, limit traffic, limit expiration time

View File

@@ -151,9 +151,9 @@ class DBInbound {
} }
} }
genLink(clientIndex) { genLink(address=this.address, remark=this.remark, clientIndex=0) {
const inbound = this.toInbound(); const inbound = this.toInbound();
return inbound.genLink(this.address, this.remark, clientIndex); return inbound.genLink(address, remark, clientIndex);
} }
get genInboundLinks() { get genInboundLinks() {

View File

@@ -494,7 +494,7 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName); settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains);
} }
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
@@ -562,17 +562,19 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
}; };
TlsStreamSettings.Settings = class extends XrayCommonClass { TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, fingerprint = '', serverName = '') { constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) {
super(); super();
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.serverName = serverName; this.serverName = serverName;
this.domains = domains;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new TlsStreamSettings.Settings( return new TlsStreamSettings.Settings(
json.allowInsecure, json.allowInsecure,
json.fingerprint, json.fingerprint,
json.servername, json.serverName,
json.domains,
); );
} }
toJson() { toJson() {
@@ -580,6 +582,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
serverName: this.serverName, serverName: this.serverName,
domains: this.domains,
}; };
} }
}; };
@@ -1380,24 +1383,12 @@ class Inbound extends XrayCommonClass {
genLink(address='', remark='', clientIndex=0) { genLink(address='', remark='', clientIndex=0) {
switch (this.protocol) { switch (this.protocol) {
case Protocols.VMESS: case Protocols.VMESS:
if (this.settings.vmesses[clientIndex].email != ""){
remark = this.settings.vmesses[clientIndex].email
}
return this.genVmessLink(address, remark, clientIndex); return this.genVmessLink(address, remark, clientIndex);
case Protocols.VLESS: case Protocols.VLESS:
if (this.settings.vlesses[clientIndex].email != ""){
remark = this.settings.vlesses[clientIndex].email
}
return this.genVLESSLink(address, remark, clientIndex); return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS: case Protocols.SHADOWSOCKS:
if (this.settings.shadowsockses[clientIndex].email != ""){
remark = this.settings.shadowsockses[clientIndex].email
}
return this.genSSLink(address, remark, clientIndex); return this.genSSLink(address, remark, clientIndex);
case Protocols.TROJAN: case Protocols.TROJAN:
if (this.settings.trojans[clientIndex].email != ""){
remark = this.settings.trojans[clientIndex].email
}
return this.genTrojanLink(address, remark, clientIndex); return this.genTrojanLink(address, remark, clientIndex);
default: return ''; default: return '';
} }
@@ -1409,12 +1400,17 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS: case Protocols.VMESS:
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
JSON.parse(this.settings).clients.forEach((_,index) => { case Protocols.SHADOWSOCKS:
link += this.genLink(address, remark, index) + '\r\n'; JSON.parse(this.settings).clients.forEach((client,index) => {
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
this.stream.tls.settings.domains.forEach((domain) => {
link += this.genLink(domain.domain, remark + '-' + client.email + '-' + domain.remark, index) + '\r\n';
});
} else {
link += this.genLink(address, remark + '-' + client.email, index) + '\r\n';
}
}); });
return link; return link;
case Protocols.SHADOWSOCKS:
return (this.genSSLink(address, remark) + '\r\n');
default: return ''; default: return '';
} }
} }

View File

@@ -4,48 +4,54 @@
:class="themeSwitcher.darkCardClass" :class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="300px"> width="300px">
<a-tag v-if="qrModal.clientName" color="orange" style="margin-bottom: 10px;display: block;text-align: center;">
{{ i18n "pages.inbounds.email" }}: "[[ qrModal.clientName ]]"
</a-tag>
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag> <a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
<template v-if="app.subSettings.enable && qrModal.subId">
<a-divider>Subscription</a-divider>
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas>
</template>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
</template>
</a-modal> </a-modal>
<script> <script>
const qrModal = { const qrModal = {
title: '', title: '',
content: '', clientIndex: 0,
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
copyText: '', client: null,
clientName: null, qrcodes: [],
qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '', clientName = null) { subId: '',
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
this.title = title; this.title = title;
this.content = content; this.clientIndex = clientIndex;
this.dbInbound = dbInbound; this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.clientName = clientName; settings = JSON.parse(this.inbound.settings);
if (ObjectUtil.isEmpty(copyText)) { this.client = settings.clients[clientIndex];
this.copyText = content; remark = this.dbInbound.remark + "-" + this.client.email;
address = this.dbInbound.address;
this.qrcodes = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
this.qrcodes.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
});
});
} else { } else {
this.copyText = copyText; this.qrcodes.push({
remark: remark,
link: this.inbound.genLink(address, remark, clientIndex)
});
} }
this.visible = true; this.visible = true;
qrModalApp.$nextTick(() => {
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
});
}, },
close: function () { close: function () {
this.visible = false; this.visible = false;
@@ -59,16 +65,40 @@
qrModal: qrModal, qrModal: qrModal,
}, },
methods: { methods: {
copyToClipboard() { copyToClipboard(elmentId,content) {
this.qrModal.clipboard = new ClipboardJS('#qrCode', { this.qrModal.clipboard = new ClipboardJS('#'+elmentId, {
text: () => this.qrModal.copyText, text: () => content,
}); });
this.qrModal.clipboard.on('success', () => { this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}') app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy(); this.qrModal.clipboard.destroy();
}); });
},
setQrCode(elmentId,content) {
new QRious({
element: document.querySelector('#'+elmentId),
size: 260,
value: content,
});
},
genSubLink(subID) {
protocol = app.subSettings.tls ? "https://" : "http://";
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
subPort = app.subSettings.port;
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
subPath = app.subSettings.path;
return protocol + hostName + port + subPath + subID;
} }
}, },
updated() {
if (qrModal.client.subId){
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub",this.genSubLink(this.subId));
}
qrModal.qrcodes.forEach((element,index) => {
this.setQrCode("qrCode-"+index, element.link);
});
}
}); });
</script> </script>

View File

@@ -65,6 +65,27 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Multi Domain</td>
<td>
<a-switch v-model="multiDomain"></a-switch>
<a-button v-if="multiDomain" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button>
</td>
</tr>
<tr v-if="multiDomain">
<td colspan="2">
<a-form-item>
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
<template slot="addonAfter">
<a-button size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>{{ i18n "domainName" }}</td> <td>{{ i18n "domainName" }}</td>
<td> <td>
<a-form-item> <a-form-item>

View File

@@ -175,10 +175,14 @@
</template> </template>
<div v-if="dbInbound.hasLink()"> <div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider> <a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p> <a-row v-for="(link,index) in infoModal.links">
<button class="ant-btn ant-btn-primary" id="copy-url-link" @click="copyToClipboard('copy-url-link', infoModal.link)"> <a-col :span="21"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-icon type="snippets"></a-icon>{{ i18n "copy" }} <a-col :span="3" style="text-align: right;">
</button> <button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
</button>
</a-col>
</a-row>
</div> </div>
</a-modal> </a-modal>
<script> <script>
@@ -192,7 +196,7 @@
upStats: 0, upStats: 0,
downStats: 0, downStats: 0,
clipboard: null, clipboard: null,
link: null, links: [],
index: null, index: null,
isExpired: false, isExpired: false,
subLink: '', subLink: '',
@@ -201,11 +205,26 @@
this.index = index; this.index = index;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.link = dbInbound.genLink(index);
this.settings = JSON.parse(this.inbound.settings); this.settings = JSON.parse(this.inbound.settings);
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null; this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index); this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
remark = this.dbInbound.remark + "-" + this.clientSettings.email;
address = this.dbInbound.address;
this.links = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
this.links.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
});
});
} else {
this.links.push({
remark: remark,
link: this.inbound.genLink(address, remark, index)
});
}
if (this.clientSettings) { if (this.clientSettings) {
if (this.clientSettings.subId) { if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId); this.subLink = this.genSubLink(this.clientSettings.subId);

View File

@@ -90,6 +90,18 @@
set delayedExpireDays(days){ set delayedExpireDays(days){
this.client.expiryTime = -86400000 * days; this.client.expiryTime = -86400000 * days;
}, },
get multiDomain() {
return this.inbound.stream.tls.settings.domains.length > 0;
},
set multiDomain(value) {
if (value) {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}];
} else {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [];
}
}
}, },
methods: { methods: {
streamNetworkChange() { streamNetworkChange() {

View File

@@ -104,6 +104,10 @@
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
<a-switch v-model="enableFilter"
checked-children="{{ i18n "search" }}" un-checked-children="{{ i18n "filter" }}"
@change="toggleFilter">
</a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input> <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid"> <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button> <a-radio-button value="">{{ i18n "none" }}</a-radio-button>
@@ -111,10 +115,6 @@
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button> <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button> <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
</a-radio-group> </a-radio-group>
<a-switch v-model="enableFilter"
checked-children="{{ i18n "search" }}" un-checked-children="{{ i18n "filter" }}"
@change="toggleFilter">
</a-switch>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds" :data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }" :loading="spinning" :scroll="{ x: 1300 }"
@@ -760,9 +760,7 @@
} }
}, },
showQrcode(dbInbound, clientIndex) { showQrcode(dbInbound, clientIndex) {
const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email; qrModal.show('{{ i18n "qrCode"}}', dbInbound, clientIndex);
const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName);
}, },
showInfo(dbInbound, index) { showInfo(dbInbound, index) {
infoModal.show(dbInbound, index); infoModal.show(dbInbound, index);