improve reality setting

split xtls from tls - remove iran warp - remove old setting reality from franzkafka (it was a messy code) -and other improvement

Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
This commit is contained in:
MHSanaei 2023-04-18 21:34:06 +03:30
parent dc7dbae14a
commit 3e0faecaae
20 changed files with 635 additions and 332 deletions

View file

@ -246,6 +246,11 @@
background-color: #2e3b52; background-color: #2e3b52;
} }
.ant-card-dark .ant-select-disabled .ant-select-selection {
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: #242c3a;
}
.ant-card-dark .ant-collapse-item { .ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #161b22; background-color: #161b22;

View file

@ -49,6 +49,7 @@ const XTLS_FLOW_CONTROL = {
const TLS_FLOW_CONTROL = { const TLS_FLOW_CONTROL = {
VISION: "xtls-rprx-vision", VISION: "xtls-rprx-vision",
VISION_UDP443: "xtls-rprx-vision-udp443",
}; };
const TLS_VERSION_OPTION = { const TLS_VERSION_OPTION = {
@ -91,9 +92,6 @@ const UTLS_FINGERPRINT = {
UTLS_RANDOMIZED: "randomized", UTLS_RANDOMIZED: "randomized",
}; };
const bytesToHex = e => Array.from(e).map(e => e.toString(16).padStart(2, 0)).join('');
const hexToBytes = e => new Uint8Array(e.match(/[0-9a-f]{2}/gi).map(e => parseInt(e, 16)));
const ALPN_OPTION = { const ALPN_OPTION = {
H3: "h3", H3: "h3",
H2: "h2", H2: "h2",
@ -481,7 +479,7 @@ class TlsStreamSettings extends XrayCommonClass {
cipherSuites = '', cipherSuites = '',
certificates=[new TlsStreamSettings.Cert()], certificates=[new TlsStreamSettings.Cert()],
alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1], alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1],
settings=[new TlsStreamSettings.Settings()]) { settings=new TlsStreamSettings.Settings()) {
super(); super();
this.server = serverName; this.server = serverName;
this.minVersion = minVersion; this.minVersion = minVersion;
@ -508,8 +506,7 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
let values = json.settings[0]; settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName);
settings = [new TlsStreamSettings.Settings(values.allowInsecure , values.fingerprint, values.serverName)];
} }
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
@ -530,7 +527,7 @@ class TlsStreamSettings extends XrayCommonClass {
cipherSuites: this.cipherSuites, cipherSuites: this.cipherSuites,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn, alpn: this.alpn,
settings: TlsStreamSettings.toJsonArray(this.settings), settings: this.settings,
}; };
} }
} }
@ -598,71 +595,204 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
}; };
} }
}; };
class XtlsStreamSettings extends XrayCommonClass {
constructor(serverName='',
certificates=[new XtlsStreamSettings.Cert()],
alpn=[ALPN_OPTION.H2,ALPN_OPTION.HTTP1],
settings=new XtlsStreamSettings.Settings()) {
super();
this.server = serverName;
this.certs = certificates;
this.alpn = alpn;
this.settings = settings;
}
addCert(cert) {
this.certs.push(cert);
}
removeCert(index) {
this.certs.splice(index, 1);
}
static fromJson(json={}) {
let certs;
let settings;
if (!ObjectUtil.isEmpty(json.certificates)) {
certs = json.certificates.map(cert => XtlsStreamSettings.Cert.fromJson(cert));
}
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new XtlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.serverName);
}
return new XtlsStreamSettings(
json.serverName,
certs,
json.alpn,
settings,
);
}
toJson() {
return {
serverName: this.server,
certificates: XtlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn,
settings: this.settings,
};
}
}
XtlsStreamSettings.Cert = class extends XrayCommonClass {
constructor(useFile=true, certificateFile='', keyFile='', certificate='', key='') {
super();
this.useFile = useFile;
this.certFile = certificateFile;
this.keyFile = keyFile;
this.cert = certificate instanceof Array ? certificate.join('\n') : certificate;
this.key = key instanceof Array ? key.join('\n') : key;
}
static fromJson(json={}) {
if ('certificateFile' in json && 'keyFile' in json) {
return new XtlsStreamSettings.Cert(
true,
json.certificateFile,
json.keyFile,
);
} else {
return new XtlsStreamSettings.Cert(
false, '', '',
json.certificate.join('\n'),
json.key.join('\n'),
);
}
}
toJson() {
if (this.useFile) {
return {
certificateFile: this.certFile,
keyFile: this.keyFile,
};
} else {
return {
certificate: this.cert.split('\n'),
key: this.key.split('\n'),
};
}
}
};
XtlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, serverName = '') {
super();
this.allowInsecure = allowInsecure;
this.serverName = serverName;
}
static fromJson(json = {}) {
return new XtlsStreamSettings.Settings(
json.allowInsecure,
json.servername,
);
}
toJson() {
return {
allowInsecure: this.allowInsecure,
serverName: this.serverName,
};
}
};
class RealityStreamSettings extends XrayCommonClass { class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false,xver = 0, show = false,xver = 0,
fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX,
dest = 'yahoo.com:443', dest = 'yahoo.com:443',
serverNames = 'yahoo.com,www.yahoo.com', serverNames = 'yahoo.com,www.yahoo.com',
privateKey = RandomUtil.randomX25519PrivateKey(), privateKey = '',
publicKey = '',
minClient = '', minClient = '',
maxClient = '', maxClient = '',
maxTimediff = 0, maxTimediff = 0,
shortIds = RandomUtil.randowShortId() shortIds = RandomUtil.randowShortId(),
) settings= new RealityStreamSettings.Settings()
{ ){
super(); super();
this.show = show; this.show = show;
this.xver = xver; this.xver = xver;
this.fingerprint = fingerprint;
this.dest = dest; this.dest = dest;
this.serverNames = serverNames instanceof Array ? serverNames.join(",") : serverNames; this.serverNames = serverNames instanceof Array ? serverNames.join(",") : serverNames;
this.privateKey = privateKey; this.privateKey = privateKey;
this.publicKey = RandomUtil.randomX25519PublicKey(this.privateKey);
this.minClient = minClient; this.minClient = minClient;
this.maxClient = maxClient; this.maxClient = maxClient;
this.maxTimediff = maxTimediff; this.maxTimediff = maxTimediff;
this.shortIds = shortIds instanceof Array ? shortIds.join(",") : shortIds; this.shortIds = shortIds instanceof Array ? shortIds.join(",") : shortIds;
this.settings = settings;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
let settings;
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName);
}
return new RealityStreamSettings( return new RealityStreamSettings(
json.show, json.show,
json.xver, json.xver,
json.fingerprint,
json.dest, json.dest,
json.serverNames, json.serverNames,
json.privateKey, json.privateKey,
json.publicKey,
json.minClient, json.minClient,
json.maxClient, json.maxClient,
json.maxTimediff, json.maxTimediff,
json.shortIds json.shortIds,
json.settings,
); );
} }
toJson() { toJson() {
return { return {
show: this.show, show: this.show,
xver: this.xver, xver: this.xver,
fingerprint: this.fingerprint,
dest: this.dest, dest: this.dest,
serverNames: this.serverNames.split(/,||\s+/), serverNames: this.serverNames.split(","),
privateKey: this.privateKey, privateKey: this.privateKey,
publicKey: this.publicKey,
minClient: this.minClient, minClient: this.minClient,
maxClient: this.maxClient, maxClient: this.maxClient,
maxTimediff: this.maxTimediff, maxTimediff: this.maxTimediff,
shortIds: this.shortIds.split(/,||\s+/) shortIds: this.shortIds.split(","),
settings: this.settings,
}; };
} }
}
RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
} }
static fromJson(json = {}) {
return new RealityStreamSettings.Settings(
json.publicKey,
json.fingerprint,
json.serverName,
);
}
toJson() {
return {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
};
}
};
class StreamSettings extends XrayCommonClass { class StreamSettings extends XrayCommonClass {
constructor(network='tcp', constructor(network='tcp',
security='none', security='none',
tlsSettings=new TlsStreamSettings(), tlsSettings=new TlsStreamSettings(),
xtlsSettings=new XtlsStreamSettings(),
realitySettings = new RealityStreamSettings(), realitySettings = new RealityStreamSettings(),
tcpSettings=new TcpStreamSettings(), tcpSettings=new TcpStreamSettings(),
kcpSettings=new KcpStreamSettings(), kcpSettings=new KcpStreamSettings(),
@ -675,6 +805,7 @@ class StreamSettings extends XrayCommonClass {
this.network = network; this.network = network;
this.security = security; this.security = security;
this.tls = tlsSettings; this.tls = tlsSettings;
this.xtls = xtlsSettings;
this.reality = realitySettings; this.reality = realitySettings;
this.tcp = tcpSettings; this.tcp = tcpSettings;
this.kcp = kcpSettings; this.kcp = kcpSettings;
@ -685,7 +816,7 @@ class StreamSettings extends XrayCommonClass {
} }
get isTls() { get isTls() {
return this.security === 'tls'; return this.security === "tls";
} }
set isTls(isTls) { set isTls(isTls) {
@ -696,12 +827,12 @@ class StreamSettings extends XrayCommonClass {
} }
} }
get isXTLS() { get isXtls() {
return this.security === "xtls"; return this.security === "xtls";
} }
set isXTLS(isXTLS) { set isXtls(isXtls) {
if (isXTLS) { if (isXtls) {
this.security = 'xtls'; this.security = 'xtls';
} else { } else {
this.security = 'none'; this.security = 'none';
@ -715,27 +846,19 @@ class StreamSettings extends XrayCommonClass {
set isReality(isReality) { set isReality(isReality) {
if (isReality) { if (isReality) {
this.security = "reality"; this.security = 'reality';
} else { } else {
this.security = "none"; this.security = 'none';
} }
} }
static fromJson(json = {}) { static fromJson(json={}) {
let tls, reality;
if (json.security === "xtls") {
tls = TlsStreamSettings.fromJson(json.XTLSSettings);
} else if (json.security === "tls") {
tls = TlsStreamSettings.fromJson(json.tlsSettings);
}
if (json.security === "reality") {
reality = RealityStreamSettings.fromJson(json.realitySettings)
}
return new StreamSettings( return new StreamSettings(
json.network, json.network,
json.security, json.security,
tls, TlsStreamSettings.fromJson(json.tlsSettings),
reality, XtlsStreamSettings.fromJson(json.xtlsSettings),
RealityStreamSettings.fromJson(json.realitySettings),
TcpStreamSettings.fromJson(json.tcpSettings), TcpStreamSettings.fromJson(json.tcpSettings),
KcpStreamSettings.fromJson(json.kcpSettings), KcpStreamSettings.fromJson(json.kcpSettings),
WsStreamSettings.fromJson(json.wsSettings), WsStreamSettings.fromJson(json.wsSettings),
@ -751,9 +874,9 @@ class StreamSettings extends XrayCommonClass {
network: network, network: network,
security: this.security, security: this.security,
tlsSettings: this.isTls ? this.tls.toJson() : undefined, tlsSettings: this.isTls ? this.tls.toJson() : undefined,
XTLSSettings: this.isXTLS ? this.tls.toJson() : undefined, xtlsSettings: this.isXtls ? this.xtls.toJson() : undefined,
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
realitySettings: this.isReality ? this.reality.toJson() : undefined, realitySettings: this.isReality ? this.reality.toJson() : undefined,
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined, kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
wsSettings: network === 'ws' ? this.ws.toJson() : undefined, wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
httpSettings: network === 'http' ? this.http.toJson() : undefined, httpSettings: network === 'http' ? this.http.toJson() : undefined,
@ -826,22 +949,18 @@ class Inbound extends XrayCommonClass {
set tls(isTls) { set tls(isTls) {
if (isTls) { if (isTls) {
this.xtls = false;
this.reality = false;
this.stream.security = 'tls'; this.stream.security = 'tls';
} else { } else {
this.stream.security = 'none'; this.stream.security = 'none';
} }
} }
get XTLS() { get xtls() {
return this.stream.security === 'xtls'; return this.stream.security === 'xtls';
} }
set XTLS(isXTLS) { set xtls(isXtls) {
if (isXTLS) { if (isXtls) {
this.xtls = false;
this.reality = false;
this.stream.security = 'xtls'; this.stream.security = 'xtls';
} else { } else {
this.stream.security = 'none'; this.stream.security = 'none';
@ -850,19 +969,14 @@ class Inbound extends XrayCommonClass {
//for Reality //for Reality
get reality() { get reality() {
if (this.stream.security === "reality") { return this.stream.security === 'reality';
return this.network === "tcp" || this.network === "grpc" || this.network === "http";
}
return false;
} }
set reality(isReality) { set reality(isReality) {
if (isReality) { if (isReality) {
this.tls = false; this.stream.security = 'reality';
this.xtls = false;
this.stream.security = "reality";
} else { } else {
this.stream.security = "none"; this.stream.security = 'none';
} }
} }
@ -969,7 +1083,7 @@ class Inbound extends XrayCommonClass {
} }
get serverName() { get serverName() {
if (this.stream.isTls || this.stream.isXTLS) { if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) {
return this.stream.tls.server; return this.stream.tls.server;
} }
return ""; return "";
@ -1070,7 +1184,14 @@ class Inbound extends XrayCommonClass {
default: default:
return false; return false;
} }
return this.network === "tcp" || this.network === "grpc" || this.network === "http"; switch (this.network) {
case "tcp":
case "http":
case "grpc":
return true;
default:
return false;
}
} }
//this is used for xtls-rprx-vision //this is used for xtls-rprx-vision
@ -1090,7 +1211,7 @@ class Inbound extends XrayCommonClass {
return this.canEnableTls(); return this.canEnableTls();
} }
canEnableXTLS() { canEnableXtls() {
switch (this.protocol) { switch (this.protocol) {
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
@ -1195,10 +1316,10 @@ class Inbound extends XrayCommonClass {
host: host, host: host,
path: path, path: path,
tls: this.stream.security, tls: this.stream.security,
sni: this.stream.tls.settings[0]['serverName'], sni: this.stream.tls.settings.serverName,
fp: this.stream.tls.settings[0]['fingerprint'], fp: this.stream.tls.settings.fingerprint,
alpn: this.stream.tls.alpn.join(','), alpn: this.stream.tls.alpn.join(','),
allowInsecure: this.stream.tls.settings[0].allowInsecure, allowInsecure: this.stream.tls.settings.allowInsecure,
}; };
return 'vmess://' + base64(JSON.stringify(obj, null, 2)); return 'vmess://' + base64(JSON.stringify(obj, null, 2));
} }
@ -1257,51 +1378,51 @@ class Inbound extends XrayCommonClass {
if (this.tls) { if (this.tls) {
params.set("security", "tls"); params.set("security", "tls");
params.set("fp" , this.stream.tls.settings[0]['fingerprint']); params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn); params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){ if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (!ObjectUtil.isEmpty(this.stream.tls.server)) { if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server; address = this.stream.tls.server;
} }
if (this.stream.tls.settings[0]['serverName'] !== ''){ if (this.stream.tls.settings.serverName !== ''){
params.set("sni", this.stream.tls.settings[0]['serverName']); params.set("sni", this.stream.tls.settings.serverName);
} }
if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) { if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
params.set("flow", this.settings.vlesses[clientIndex].flow); params.set("flow", this.settings.vlesses[clientIndex].flow);
} }
} }
if (this.XTLS) { if (this.xtls) {
params.set("security", "xtls"); params.set("security", "xtls");
params.set("alpn", this.stream.tls.alpn); params.set("alpn", this.stream.xtls.alpn);
if(this.stream.tls.settings[0].allowInsecure){ if(this.stream.xtls.settings.allowInsecure){
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (!ObjectUtil.isEmpty(this.stream.tls.server)) { if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
address = this.stream.tls.server; address = this.stream.xtls.server;
} }
params.set("flow", this.settings.vlesses[clientIndex].flow); params.set("flow", this.settings.vlesses[clientIndex].flow);
} }
if (this.reality) { if (this.reality) {
params.set("security", "reality"); params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) { if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(/,||\s+/)[0]); params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.reality.publicKey != "") {
//params.set("pbk", Ed25519.getPublicKey(this.stream.reality.privateKey));
params.set("pbk", this.stream.reality.publicKey);
} }
if (this.stream.network === 'tcp') { if (this.stream.network === 'tcp') {
params.set("flow", this.settings.vlesses[clientIndex].flow); params.set("flow", this.settings.vlesses[clientIndex].flow);
} }
if (this.stream.reality.shortIds != "") { if (this.stream.reality.shortIds != "") {
params.set("sid", this.stream.reality.shortIds); params.set("sid", this.stream.reality.shortIds.split(",")[0]);
} }
if (this.stream.reality.fingerprint != "") { if (this.stream.reality.settings.fingerprint != "") {
params.set("fp", this.stream.reality.fingerprint); params.set("fp", this.stream.reality.settings.fingerprint);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
} }
} }
@ -1376,47 +1497,47 @@ class Inbound extends XrayCommonClass {
if (this.tls) { if (this.tls) {
params.set("security", "tls"); params.set("security", "tls");
params.set("fp" , this.stream.tls.settings[0]['fingerprint']); params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn); params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){ if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (!ObjectUtil.isEmpty(this.stream.tls.server)) { if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server; address = this.stream.tls.server;
} }
if (this.stream.tls.settings[0]['serverName'] !== ''){ if (this.stream.tls.settings.serverName !== ''){
params.set("sni", this.stream.tls.settings[0]['serverName']); params.set("sni", this.stream.tls.settings.serverName);
} }
} }
if (this.reality) { if (this.reality) {
params.set("security", "reality"); params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) { if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(/,||\s+/)[0]); params.set("sni", this.stream.reality.serverNames.split(",")[0]);
} }
if (this.stream.reality.publicKey != "") { if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
//params.set("pbk", Ed25519.getPublicKey(this.stream.reality.privateKey)); address = this.stream.reality.settings.serverName;
params.set("pbk", this.stream.reality.publicKey);
}
if (this.stream.network === 'tcp') {
params.set("flow", this.settings.trojans[clientIndex].flow);
} }
if (this.stream.reality.shortIds != "") { if (this.stream.reality.shortIds != "") {
params.set("sid", this.stream.reality.shortIds); params.set("sid", this.stream.reality.shortIds.split(",")[0]);
} }
if (this.stream.reality.fingerprint != "") { if (this.stream.reality.settings.fingerprint != "") {
params.set("fp", this.stream.reality.fingerprint); params.set("fp", this.stream.reality.settings.fingerprint);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
} }
} }
if (this.XTLS) { if (this.xtls) {
params.set("security", "xtls"); params.set("security", "xtls");
params.set("alpn", this.stream.tls.alpn); params.set("alpn", this.stream.xtls.alpn);
if(this.stream.tls.settings[0].allowInsecure){ if(this.stream.xtls.settings.allowInsecure){
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (!ObjectUtil.isEmpty(this.stream.tls.server)) { if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server; address = this.stream.xtls.server;
} }
params.set("flow", this.settings.trojans[clientIndex].flow); params.set("flow", this.settings.trojans[clientIndex].flow);
} }

View file

@ -94,26 +94,6 @@ const shortIdSeq = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
]; ];
const x25519Map = new Map(
[
['EH2FWe-Ij_FFAa2u9__-aiErLvVIneP601GOCdlyPWw', "goY3OtfaA4UYbiz7Hn0NysI5QJrK0VT_Chg6RLgUPQU"],
['cKI_6DoMSP1IepeWWXrG3G9nkehl94KYBhagU50g2U0', "VigpKFbSLnHLzBWobZaS1IBmw--giJ51w92y723ajnU"],
['qM2SNyK3NyHB6deWpEP3ITyCGKQFRTna_mlKP0w1QH0', "HYyIGuyNFslmcnNT7mrDdmuXwn4cm7smE_FZbYguKHQ"],
['qCWg5GMEDFd3n1nxDswlIpOHoPUXMLuMOIiLUVzubkI', "rJFC3dUjJxMnVZiUGzmf_LFsJUwFWY-CU5RQgFOHCWM"],
['4NOBxDrEsOhNI3Y3EnVIy_TN-uyBoAjQw6QM0YsOi0s', "CbcY9qc4YuMDJDyyL0OITlU824TBg1O84ClPy27e2RM"],
['eBvFb0M4HpSOwWjtXV8zliiEs_hg56zX4a2LpuuqpEI', "CjulQ2qVIky7ImIfysgQhNX7s_drGLheCGSkVHcLZhc"],
['yEpOzQV04NNcycWVeWtRNTzv5TS-ynTuKRacZCH-6U8', "O9RSr5gSdok2K_tobQnf_scyKVqnCx6C4Jrl7_rCZEQ"],
['CNt6TAUVCwqM6xIBHyni0K3Zqbn2htKQLvLb6XDgh0s', "d9cGLVBrDFS02L2OvkqyqwFZ1Ux3AHs28ehl4Rwiyl0"],
['EInKw-6Wr0rAHXlxxDuZU5mByIzcD3Z-_iWPzXlUL1k', "LlYD2nNVAvyjNvjZGZh4R8PkMIwkc6EycPTvR2LE0nQ"],
['GKIKo7rcXVyle-EUHtGIDtYnDsI6osQmOUl3DTJRAGc', "VcqHivYGGoBkcxOI6cSSjQmneltstkb2OhvO53dyhEM"],
['-FVDzv68IC17fJVlNDlhrrgX44WeBfbhwjWpCQVXGHE', "PGG2EYOvsFt2lAQTD7lqHeRxz2KxvllEDKcUrtizPBU"],
['0H3OJEYEu6XW7woqy7cKh2vzg6YHkbF_xSDTHKyrsn4', "mzevpYbS8kXengBY5p7tt56QE4tS3lwlwRemmkcQeyc"],
['8F8XywN6ci44ES6em2Z0fYYxyptB9uaXY9Hc1WSSPE4', "qCZUdWQZ2H33vWXnOkG8NpxBeq3qn5QWXlfCOWBNkkc"],
['IN0dqfkC10dj-ifRHrg2PmmOrzYs697ajGMwcLbu-1g', "2UW_EO3r7uczPGUUlpJBnMDpDmWUHE2yDzCmXS4sckE"],
['uIcmks5rAhvBe4dRaJOdeSqgxLGGMZhsGk4J4PEKL2s', "F9WJV_74IZp0Ide4hWjiJXk9FRtBUBkUr3mzU-q1lzk"],
]
);
class RandomUtil { class RandomUtil {
static randomIntRange(min, max) { static randomIntRange(min, max) {
@ -170,26 +150,6 @@ class RandomUtil {
}); });
} }
static randowShortId() {
let str = '';
str += this.randomShortIdSeq(8)
return str;
}
static randomX25519PrivateKey() {
let num = x25519Map.size;
let index = this.randomInt(num);
let cntr = 0;
for (let key of x25519Map.keys()) {
if (cntr++ === index) {
return key;
}
}
}
static randomX25519PublicKey(key) {
return x25519Map.get(key)
}
static randomText() { static randomText() {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = ''; var string = '';
@ -199,6 +159,12 @@ class RandomUtil {
} }
return string; return string;
} }
static randowShortId() {
let str = '';
str += this.randomShortIdSeq(8)
return str;
}
} }
class ObjectUtil { class ObjectUtil {

View file

@ -33,7 +33,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
g.POST("/clientIps/:email", a.getClientIps) g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps) g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient/", a.addInboundClient) g.POST("/addClient", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient) g.POST("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient) g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic) g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
@ -151,19 +151,19 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
jsonMsg(c, "Log Cleared", nil) jsonMsg(c, "Log Cleared", nil)
} }
func (a *InboundController) addInboundClient(c *gin.Context) { func (a *InboundController) addInboundClient(c *gin.Context) {
inbound := &model.Inbound{} data := &model.Inbound{}
err := c.ShouldBind(inbound) err := c.ShouldBind(data)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return return
} }
err = a.inboundService.AddInboundClient(inbound) err = a.inboundService.AddInboundClient(data)
if err != nil { if err != nil {
jsonMsg(c, "something worng!", err) jsonMsg(c, "something worng!", err)
return return
} }
jsonMsg(c, "Client added", nil) jsonMsg(c, "Client(s) added", nil)
if err == nil { if err == nil {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }

View file

@ -41,6 +41,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/getConfigJson", a.getConfigJson) g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb) g.GET("/getDb", a.getDb)
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
} }
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
@ -114,7 +115,7 @@ func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count") count := c.Param("count")
logs, err := a.serverService.GetLogs(count) logs, err := a.serverService.GetLogs(count)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err) jsonMsg(c, "getLogs", err)
return return
} }
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
@ -123,7 +124,7 @@ func (a *ServerController) getLogs(c *gin.Context) {
func (a *ServerController) getConfigJson(c *gin.Context) { func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson() configJson, err := a.serverService.GetConfigJson()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err) jsonMsg(c, "get config.json", err)
return return
} }
jsonObj(c, configJson, nil) jsonObj(c, configJson, nil)
@ -132,7 +133,7 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
func (a *ServerController) getDb(c *gin.Context) { func (a *ServerController) getDb(c *gin.Context) {
db, err := a.serverService.GetDb() db, err := a.serverService.GetDb()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err) jsonMsg(c, "get Database", err)
return return
} }
// Set the headers for the response // Set the headers for the response
@ -142,3 +143,12 @@ func (a *ServerController) getDb(c *gin.Context) {
// Write the file contents to the response // Write the file contents to the response
c.Writer.Write(db) c.Writer.Write(db)
} }
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert()
if err != nil {
jsonMsg(c, "get x25519 certificate", err)
return
}
jsonObj(c, cert, nil)
}

View file

@ -29,14 +29,18 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
func (a *SUBController) subs(c *gin.Context) { func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0] host := strings.Split(c.Request.Host, ":")[0]
subs, err := a.subService.GetSubs(subId, host) subs, header, err := a.subService.GetSubs(subId, host)
if err != nil { if err != nil || len(subs) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
result := "" result := ""
for _, sub := range subs { for _, sub := range subs {
result += sub + "\n" result += sub + "\n"
} }
// Add subscription-userinfo
c.Writer.Header().Set("subscription-userinfo", header)
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} }
} }

View file

@ -33,6 +33,30 @@
<span slot="label">{{ i18n "pages.client.clientCount" }}</span> <span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number> <a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="clientsBulkModal.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
<a-select v-model="clientsBulkModal.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="clientsBulkModal.flow" style="width: 150px">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Subscription"> <a-form-item label="Subscription">
<a-input v-model.trim="clientsBulkModal.subId"></a-input> <a-input v-model.trim="clientsBulkModal.subId"></a-input>
</a-form-item> </a-form-item>
@ -51,10 +75,10 @@
</span> </span>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number> <a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="{{ i18n "pages.client.delayedStart" }}"> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch> <a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientsBulkModal.delayedStart"> <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input> <a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
@ -83,9 +107,9 @@
confirm: null, confirm: null,
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
inbound: new Inbound(), inbound: new Inbound(),
clients: [],
quantity: 1, quantity: 1,
totalGB: 0, totalGB: 0,
limitIp: 0,
expiryTime: '', expiryTime: '',
emailMethod: 0, emailMethod: 0,
firstNum: 1, firstNum: 1,
@ -94,8 +118,10 @@
emailPostfix: "", emailPostfix: "",
subId: "", subId: "",
tgId: "", tgId: "",
flow: "",
delayedStart: false, delayedStart: false,
ok() { ok() {
clients = [];
method=clientsBulkModal.emailMethod; method=clientsBulkModal.emailMethod;
if(method>1){ if(method>1){
start=clientsBulkModal.firstNum; start=clientsBulkModal.firstNum;
@ -113,11 +139,18 @@
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix; newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient.subId = clientsBulkModal.subId; newClient.subId = clientsBulkModal.subId;
newClient.tgId = clientsBulkModal.tgId; newClient.tgId = clientsBulkModal.tgId;
newClient.limitIp = clientsBulkModal.limitIp;
newClient._totalGB = clientsBulkModal.totalGB; newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime; newClient._expiryTime = clientsBulkModal.expiryTime;
clientsBulkModal.clients.push(newClient); if(clientsBulkModal.inbound.canEnableTlsFlow()){
newClient.flow = clientsBulkModal.flow;
} }
ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound); if(clientsBulkModal.inbound.xtls){
newClient.flow = clientsBulkModal.flow;
}
clients.push(newClient);
}
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
}, },
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) { show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
this.visible = true; this.visible = true;
@ -128,15 +161,16 @@
this.totalGB = 0; this.totalGB = 0;
this.expiryTime = 0; this.expiryTime = 0;
this.emailMethod= 0; this.emailMethod= 0;
this.limitIp= 0;
this.firstNum= 1; this.firstNum= 1;
this.lastNum= 1; this.lastNum= 1;
this.emailPrefix= ""; this.emailPrefix= "";
this.emailPostfix= ""; this.emailPostfix= "";
this.subId= ""; this.subId= "";
this.tgId= ""; this.tgId= "";
this.flow= "";
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.delayedStart = false; this.delayedStart = false;
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {

View file

@ -12,6 +12,7 @@
confirmLoading: false, confirmLoading: false,
title: '', title: '',
okText: '', okText: '',
isEdit: false,
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
inbound: new Inbound(), inbound: new Inbound(),
clients: [], clients: [],
@ -21,9 +22,13 @@
isExpired: false, isExpired: false,
delayedStart: false, delayedStart: false,
ok() { ok() {
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index); if(clientModal.isEdit){
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.index);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
}, },
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=(index, dbInbound)=>{}, isEdit=false }) { show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) {
this.visible = true; this.visible = true;
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;

View file

@ -68,7 +68,7 @@
</a-textarea> </a-textarea>
</a-form> </a-form>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.XTLS" label="Flow"> <a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
@ -100,10 +100,10 @@
</a-tag> </a-tag>
</template> </template>
</a-form-item> </a-form-item>
<a-form-item label="{{ i18n "pages.client.delayedStart" }}"> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch> <a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientModal.delayedStart"> <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart">
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input> <a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>

View file

@ -1,7 +1,7 @@
{{define "form/trojan"}} {{define "form/trojan"}}
<a-form layout="inline"> <a-form layout="inline">
<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" }}'>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@ -31,7 +31,7 @@
</span> </span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input> <a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.XTLS" label="Flow"> <a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>

View file

@ -1,7 +1,7 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-form layout="inline"> <a-form layout="inline">
<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" }}'>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@ -31,7 +31,7 @@
</span> </span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input> <a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.XTLS" label="Flow"> <a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>

View file

@ -1,7 +1,7 @@
{{define "form/vmess"}} {{define "form/vmess"}}
<a-form layout="inline"> <a-form layout="inline">
<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" }}'>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">

View file

@ -17,7 +17,7 @@
</span> </span>
<a-switch v-model="inbound.reality"></a-switch> <a-switch v-model="inbound.reality"></a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.canEnableXTLS()"> <a-form-item v-if="inbound.canEnableXtls()">
<span slot="label"> <span slot="label">
XTLS XTLS
<a-tooltip> <a-tooltip>
@ -27,14 +27,14 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-switch v-model="inbound.XTLS"></a-switch> <a-switch v-model="inbound.xtls"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- tls settings --> <!-- tls settings -->
<a-form v-if="inbound.tls || inbound.XTLS" layout="inline"> <a-form v-if="inbound.tls" layout="inline">
<a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls"> <a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input> <a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="CipherSuites"> <a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px"> <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
@ -52,22 +52,22 @@
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="uTLS" v-if="inbound.tls" > <a-form-item label="SNI" placeholder="Server Name Indication">
<a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px"> <a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 170px">
<a-select-option value=''>None</a-select-option> <a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
</a-form-item>
<a-form-item label="Alpn"> <a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px"> <a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox> <a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
</a-checkbox-group> </a-checkbox-group>
</a-form-item> </a-form-item>
<a-form-item label="Allow insecure"> <a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings[0].allowInsecure"></a-switch> <a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "certificate" }}'> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid"> <a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
@ -93,33 +93,79 @@
</a-form-item> </a-form-item>
</template> </template>
</a-form> </a-form>
<!-- xtls settings -->
<a-form v-if="inbound.xtls" layout="inline">
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.xtls.server"></a-input>
</a-form-item>
<a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.xtls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.xtls.certs[0].useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="inbound.stream.xtls.certs[0].useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].key"></a-input>
</a-form-item>
</template>
</a-form>
<!-- reality settings -->
<a-form v-else-if="inbound.reality" layout="inline"> <a-form v-else-if="inbound.reality" layout="inline">
<a-form-item label="show"> <a-form-item label="Show">
<a-switch v-model="inbound.stream.reality.show"> <a-switch v-model="inbound.stream.reality.show">
</a-switch> </a-switch>
</a-form-item> </a-form-item>
<a-form-item label="xver"> <a-form-item label="xVer">
<a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input> <a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS" > <a-form-item label="uTLS" >
<a-select v-model="inbound.stream.reality.fingerprint" style="width: 135px"> <a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 135px">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item>
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="dest"> <a-form-item label="dest">
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 360px"></a-input> <a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="serverNames"> <a-form-item label="Server Names">
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 360px"></a-input> <a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 300px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="privateKey"> <a-form-item label="ShortIds">
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 360px"></a-input>
</a-form-item>
<a-form-item label="publicKey">
<a-input v-model.trim="inbound.stream.reality.publicKey" style="width: 360px"></a-input>
</a-form-item>
<a-form-item label="shortIds">
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input> <a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Private Key">
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="Public Key">
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input>
</a-form-item>
<a-form-item >
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View file

@ -49,10 +49,14 @@
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td> </td>
<td v-else-if="inbound.XTLS"> <td v-else-if="inbound.xtls">
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td> </td>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag> <td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td> </td>
</tr> </tr>

View file

@ -43,6 +43,14 @@
loading(loading) { loading(loading) {
inModal.confirmLoading = loading; inModal.confirmLoading = loading;
}, },
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
}; };
const protocols = { const protocols = {
@ -62,6 +70,7 @@
inModal: inModal, inModal: inModal,
Protocols: protocols, Protocols: protocols,
SSMethods: SSMethods, SSMethods: SSMethods,
delayedStart: false,
get inbound() { get inbound() {
return inModal.inbound; return inModal.inbound;
}, },
@ -70,36 +79,40 @@
}, },
get isEdit() { get isEdit() {
return inModal.isEdit; return inModal.isEdit;
} },
get client() {
return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days){
this.client.expiryTime = -86400000 * days;
},
}, },
methods: { methods: {
streamNetworkChange(oldValue) { streamNetworkChange() {
if (oldValue === 'kcp') { if (!inModal.inbound.canSetTls()) {
this.inModal.inbound.tls = false; this.inModal.inbound.stream.security = 'none';
} }
}, if (!inModal.inbound.canEnableReality()) {
addClient(protocol, clients) { this.inModal.inbound.reality = false;
switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
default: return null;
} }
}, },
removeClient(index, clients) {
clients.splice(index, 1);
},
isExpiry(index) {
return this.inbound.isExpiry(index)
},
isClientEnable(email) {
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
},
setDefaultCertData(){ setDefaultCertData(){
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert; inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey; inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
}, },
async getNewX25519Cert(){
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert');
inModal.loading(false);
if (!msg.success) {
return;
}
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
getNewEmail(client) { getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = ''; var string = '';

View file

@ -133,26 +133,26 @@
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag> <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXTLS" color="cyan">XTLS</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</a-tag>
</template> </template>
</template> </template>
<template slot="clients" slot-scope="text, dbInbound"> <template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]"> <template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag> <a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template> </template>
@ -531,9 +531,9 @@
title: '{{ i18n "pages.client.add"}}', title: '{{ i18n "pages.client.add"}}',
okText: '{{ i18n "pages.client.submitAdd"}}', okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound, dbInbound: dbInbound,
confirm: async (inbound, dbInbound, index) => { confirm: async (clients, dbInboundId) => {
clientModal.loading(); clientModal.loading();
await this.addClient(inbound, dbInbound); await this.addClient(clients, dbInboundId);
clientModal.close(); clientModal.close();
}, },
isEdit: false isEdit: false
@ -545,9 +545,9 @@
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark, title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
okText: '{{ i18n "pages.client.bulk"}}', okText: '{{ i18n "pages.client.bulk"}}',
dbInbound: dbInbound, dbInbound: dbInbound,
confirm: async (inbound, dbInbound) => { confirm: async (clients, dbInboundId) => {
clientsBulkModal.loading(); clientsBulkModal.loading();
await this.addClient(inbound, dbInbound); await this.addClient(clients, dbInboundId);
clientsBulkModal.close(); clientsBulkModal.close();
}, },
}); });
@ -561,9 +561,9 @@
okText: '{{ i18n "pages.client.submitEdit"}}', okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbound: dbInbound, dbInbound: dbInbound,
index: index, index: index,
confirm: async (inbound, dbInbound, index) => { confirm: async (client, dbInboundId, index) => {
clientModal.loading(); clientModal.loading();
await this.updateClient(inbound, dbInbound, index); await this.updateClient(client, dbInboundId, index);
clientModal.close(); clientModal.close();
}, },
isEdit: true isEdit: true
@ -573,17 +573,17 @@
firstKey = Object.keys(client)[0]; firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]); return clients.findIndex(c => c[firstKey] === client[firstKey]);
}, },
async addClient(inbound, dbInbound) { async addClient(clients, dbInboundId) {
const data = { const data = {
id: dbInbound.id, id: dbInboundId,
settings: inbound.settings.toString(), settings: '{"clients": [' + clients.toString() +']}',
}; };
await this.submit('/xui/inbound/addClient/', data); await this.submit(`/xui/inbound/addClient`, data);
}, },
async updateClient(inbound, dbInbound, index) { async updateClient(client, dbInboundId, index) {
const data = { const data = {
id: dbInbound.id, id: dbInboundId,
settings: inbound.settings.toString(), settings: '{"clients": [' + client.toString() +']}',
}; };
await this.submit(`/xui/inbound/updateClient/${index}`, data); await this.submit(`/xui/inbound/updateClient/${index}`, data);
}, },

View file

@ -125,7 +125,6 @@
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.setting.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.setting.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.setting.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.setting.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRWARP"}}' desc='{{ i18n "pages.setting.xrayConfigIRWARPDesc"}}' v-model="IRWARPSettings"></setting-list-item>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
@ -672,23 +671,6 @@
}); });
}, },
}, },
IRWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.ir
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.ir
});
},
},
} }
}); });

View file

@ -64,28 +64,45 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
return clients, nil return clients, nil
} }
func (s *InboundService) checkEmailsExist(emails map[string]bool, ignoreId int) (string, error) { func (s *InboundService) getAllEmails() ([]string, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var emails []string
db = db.Model(model.Inbound{}).Where("Protocol in ?", []model.Protocol{model.VMess, model.VLESS, model.Trojan}) err := db.Raw(`
if ignoreId > 0 { SELECT JSON_EXTRACT(client.value, '$.email')
db = db.Where("id != ?", ignoreId) FROM inbounds,
} JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
db = db.Find(&inbounds) `).Scan(&emails).Error
if db.Error != nil {
return "", db.Error
}
for _, inbound := range inbounds { if err != nil {
clients, err := s.getClients(inbound) return nil, err
}
return emails, nil
}
func (s *InboundService) contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) {
allEmails, err := s.getAllEmails()
if err != nil { if err != nil {
return "", err return "", err
} }
var emails []string
for _, client := range clients { for _, client := range clients {
if emails[client.Email] { if client.Email != "" {
if s.contains(emails, client.Email) {
return client.Email, nil return client.Email, nil
} }
if s.contains(allEmails, client.Email) {
return client.Email, nil
}
emails = append(emails, client.Email)
} }
} }
return "", nil return "", nil
@ -96,16 +113,23 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
if err != nil { if err != nil {
return "", err return "", err
} }
emails := make(map[string]bool) allEmails, err := s.getAllEmails()
if err != nil {
return "", err
}
var emails []string
for _, client := range clients { for _, client := range clients {
if client.Email != "" { if client.Email != "" {
if emails[client.Email] { if s.contains(emails, client.Email) {
return client.Email, nil return client.Email, nil
} }
emails[client.Email] = true if s.contains(allEmails, client.Email) {
return client.Email, nil
}
emails = append(emails, client.Email)
} }
} }
return s.checkEmailsExist(emails, inbound.Id) return "", nil
} }
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) { func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) {
@ -215,14 +239,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
return inbound, common.NewError("Port already exists:", inbound.Port) return inbound, common.NewError("Port already exists:", inbound.Port)
} }
existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil {
return inbound, err
}
if existEmail != "" {
return inbound, common.NewError("Duplicate email:", existEmail)
}
oldInbound, err := s.GetInbound(inbound.Id) oldInbound, err := s.GetInbound(inbound.Id)
if err != nil { if err != nil {
return inbound, err return inbound, err
@ -245,8 +261,12 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
return inbound, db.Save(oldInbound).Error return inbound, db.Save(oldInbound).Error
} }
func (s *InboundService) AddInboundClient(inbound *model.Inbound) error { func (s *InboundService) AddInboundClient(data *model.Inbound) error {
existEmail, err := s.checkEmailExistForInbound(inbound) clients, err := s.getClients(data)
if err != nil {
return err
}
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil { if err != nil {
return err return err
} }
@ -255,29 +275,35 @@ func (s *InboundService) AddInboundClient(inbound *model.Inbound) error {
return common.NewError("Duplicate email:", existEmail) return common.NewError("Duplicate email:", existEmail)
} }
clients, err := s.getClients(inbound) oldInbound, err := s.GetInbound(data.Id)
if err != nil { if err != nil {
return err return err
} }
oldInbound, err := s.GetInbound(inbound.Id) var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil { if err != nil {
return err return err
} }
oldClients, err := s.getClients(oldInbound) oldClients := settings["clients"].([]interface{})
var newClients []interface{}
for _, client := range clients {
newClients = append(newClients, client)
}
settings["clients"] = append(oldClients, newClients...)
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil { if err != nil {
return err return err
} }
oldInbound.Settings = inbound.Settings oldInbound.Settings = string(newSettings)
if len(clients[len(clients)-1].Email) > 0 { for _, client := range clients {
s.AddClientStat(inbound.Id, &clients[len(clients)-1]) if len(client.Email) > 0 {
} s.AddClientStat(data.Id, &client)
for i := len(oldClients); i < len(clients); i++ {
if len(clients[i].Email) > 0 {
s.AddClientStat(inbound.Id, &clients[i])
} }
} }
db := database.GetDB() db := database.GetDB()
@ -309,21 +335,13 @@ func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string)
return db.Save(oldInbound).Error return db.Save(oldInbound).Error
} }
func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int) error { func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) error {
existEmail, err := s.checkEmailExistForInbound(inbound) clients, err := s.getClients(data)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
if err != nil { if err != nil {
return err return err
} }
oldInbound, err := s.GetInbound(inbound.Id) oldInbound, err := s.GetInbound(data.Id)
if err != nil { if err != nil {
return err return err
} }
@ -333,13 +351,40 @@ func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int)
return err return err
} }
oldInbound.Settings = inbound.Settings if len(clients[0].Email) > 0 && clients[0].Email != oldClients[index].Email {
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil {
return err
}
settingsClients := settings["clients"].([]interface{})
var newClients []interface{}
newClients = append(newClients, clients[0])
settingsClients[index] = newClients[0]
settings["clients"] = settingsClients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB() db := database.GetDB()
if len(clients[index].Email) > 0 { if len(clients[0].Email) > 0 {
if len(oldClients[index].Email) > 0 { if len(oldClients[index].Email) > 0 {
err = s.UpdateClientStat(oldClients[index].Email, &clients[index]) err = s.UpdateClientStat(oldClients[index].Email, &clients[0])
if err != nil { if err != nil {
return err return err
} }
@ -348,7 +393,7 @@ func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int)
return err return err
} }
} else { } else {
s.AddClientStat(inbound.Id, &clients[index]) s.AddClientStat(data.Id, &clients[0])
} }
} else { } else {
err = s.DelClientStat(db, oldClients[index].Email) err = s.DelClientStat(db, oldClients[index].Email)
@ -507,6 +552,16 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
count := result.RowsAffected count := result.RowsAffected
return count, err 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) RemoveOrphanedTraffics() { func (s *InboundService) RemoveOrphanedTraffics() {
db := database.GetDB() db := database.GetDB()
db.Exec(` db.Exec(`
@ -518,16 +573,6 @@ func (s *InboundService) RemoveOrphanedTraffics() {
) )
`) `)
} }
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) AddClientStat(inboundId int, client *model.Client) error { func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB() db := database.GetDB()

View file

@ -390,3 +390,29 @@ func (s *ServerService) GetDb() ([]byte, error) {
return fileContents, nil return fileContents, nil
} }
func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
privateKeyLine := strings.Split(lines[0], ":")
publicKeyLine := strings.Split(lines[1], ":")
privateKey := strings.TrimSpace(privateKeyLine[1])
publicKey := strings.TrimSpace(publicKeyLine[1])
keyPair := map[string]interface{}{
"privateKey": privateKey,
"publicKey": publicKey,
}
return keyPair, nil
}

View file

@ -8,6 +8,7 @@ import (
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger" "x-ui/logger"
"x-ui/xray"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"gorm.io/gorm" "gorm.io/gorm"
@ -18,12 +19,15 @@ type SubService struct {
inboundService InboundService inboundService InboundService
} }
func (s *SubService) GetSubs(subId string, host string) ([]string, error) { func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
s.address = host s.address = host
var result []string var result []string
var header string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId) inbounds, err := s.getInboundsBySubId(subId)
if err != nil { if err != nil {
return nil, err return nil, "", err
} }
for _, inbound := range inbounds { for _, inbound := range inbounds {
clients, err := s.inboundService.getClients(inbound) clients, err := s.inboundService.getClients(inbound)
@ -37,22 +41,60 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
if client.SubID == subId { if client.SubID == subId {
link := s.getLink(inbound, client.Email) link := s.getLink(inbound, client.Email)
result = append(result, link) result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
} }
} }
} }
return result, nil for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
traffic.Down = clientTraffic.Down
traffic.Total = clientTraffic.Total
if clientTraffic.ExpiryTime > 0 {
traffic.ExpiryTime = clientTraffic.ExpiryTime
}
} else {
traffic.Up += clientTraffic.Up
traffic.Down += clientTraffic.Down
if traffic.Total == 0 || clientTraffic.Total == 0 {
traffic.Total = 0
} else {
traffic.Total += clientTraffic.Total
}
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
traffic.ExpiryTime = 0
}
}
}
header = fmt.Sprintf("upload=%d;download=%d", traffic.Up, traffic.Down)
if traffic.Total > 0 {
header = header + fmt.Sprintf(";total=%d", traffic.Total)
}
if traffic.ExpiryTime > 0 {
header = header + fmt.Sprintf(";expire=%d", traffic.ExpiryTime)
}
return result, header, nil
} }
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
return inbounds, nil return inbounds, nil
} }
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
for _, traffic := range traffics {
if traffic.Email == email {
return traffic
}
}
return xray.ClientTraffic{}
}
func (s *SubService) getLink(inbound *model.Inbound, email string) string { func (s *SubService) getLink(inbound *model.Inbound, email string) string {
switch inbound.Protocol { switch inbound.Protocol {
case "vmess": case "vmess":
@ -296,7 +338,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if security == "xtls" { if security == "xtls" {
params["security"] = "xtls" params["security"] = "xtls"
xtlsSetting, _ := stream["XTLSSettings"].(map[string]interface{}) xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
alpns, _ := xtlsSetting["alpn"].([]interface{}) alpns, _ := xtlsSetting["alpn"].([]interface{})
var alpn []string var alpn []string
for _, a := range alpns { for _, a := range alpns {
@ -306,15 +348,15 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["alpn"] = strings.Join(alpn, ",") params["alpn"] = strings.Join(alpn, ",")
} }
XTLSSettings, _ := searchKey(xtlsSetting, "settings") xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil { if xtlsSetting != nil {
if sniValue, ok := searchKey(XTLSSettings, "serverName"); ok { if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string) params["sni"], _ = sniValue.(string)
} }
if fpValue, ok := searchKey(XTLSSettings, "fingerprint"); ok { if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string) params["fp"], _ = fpValue.(string)
} }
if insecure, ok := searchKey(XTLSSettings, "allowInsecure"); ok { if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
if insecure.(bool) { if insecure.(bool) {
params["allowInsecure"] = "1" params["allowInsecure"] = "1"
} }
@ -465,7 +507,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if security == "xtls" { if security == "xtls" {
params["security"] = "xtls" params["security"] = "xtls"
xtlsSetting, _ := stream["XTLSSettings"].(map[string]interface{}) xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
alpns, _ := xtlsSetting["alpn"].([]interface{}) alpns, _ := xtlsSetting["alpn"].([]interface{})
var alpn []string var alpn []string
for _, a := range alpns { for _, a := range alpns {
@ -475,15 +517,15 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["alpn"] = strings.Join(alpn, ",") params["alpn"] = strings.Join(alpn, ",")
} }
XTLSSettings, _ := searchKey(xtlsSetting, "settings") xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil { if xtlsSetting != nil {
if sniValue, ok := searchKey(XTLSSettings, "serverName"); ok { if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string) params["sni"], _ = sniValue.(string)
} }
if fpValue, ok := searchKey(XTLSSettings, "fingerprint"); ok { if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string) params["fp"], _ = fpValue.(string)
} }
if insecure, ok := searchKey(XTLSSettings, "allowInsecure"); ok { if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
if insecure.(bool) { if insecure.(bool) {
params["allowInsecure"] = "1" params["allowInsecure"] = "1"
} }