[feature] outbound and reverse gui

This commit is contained in:
Alireza Ahmadi
2023-11-25 16:12:26 +01:00
parent 7410b80e7a
commit 144d1e79c8
6 changed files with 1800 additions and 14 deletions

View File

@@ -0,0 +1,763 @@
const Protocols = {
Freedom: "freedom",
Blackhole: "blackhole",
DNS: "dns",
VMess: "vmess",
VLESS: "vless",
Trojan: "trojan",
Shadowsocks: "shadowsocks",
Socks: "socks",
HTTP: "http",
};
const SSMethods = {
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
const TLS_FLOW_CONTROL = {
VISION: "xtls-rprx-vision",
VISION_UDP443: "xtls-rprx-vision-udp443",
};
const UTLS_FINGERPRINT = {
UTLS_CHROME: "chrome",
UTLS_FIREFOX: "firefox",
UTLS_SAFARI: "safari",
UTLS_IOS: "ios",
UTLS_android: "android",
UTLS_EDGE: "edge",
UTLS_360: "360",
UTLS_QQ: "qq",
UTLS_RANDOM: "random",
UTLS_RANDOMIZED: "randomized",
};
const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
};
const outboundDomainStrategies = [
"AsIs",
"UseIP",
"UseIPv4",
"UseIPv6"
]
Object.freeze(Protocols);
Object.freeze(SSMethods);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(ALPN_OPTION);
Object.freeze(outboundDomainStrategies);
class CommonClass {
static toJsonArray(arr) {
return arr.map(obj => obj.toJson());
}
static fromJson() {
return new CommonClass();
}
toJson() {
return this;
}
toString(format=true) {
return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
}
}
class TcpStreamSettings extends CommonClass {
constructor(type='none', host, path) {
super();
this.type = type;
this.host = host;
this.path = path;
}
static fromJson(json={}) {
let header = json.header;
if (!header) return new TcpStreamSettings();
if(header.type == 'http' && header.request){
return new TcpStreamSettings(
header.type,
header.request.headers.Host.join(','),
header.request.path.join(','),
);
}
return new TcpStreamSettings(header.type,'','');
}
toJson() {
return {
header: {
type: this.type,
request: this.type === 'http' ? {
headers: {
Host: ObjectUtil.isEmpty(this.host) ? [] : this.host.split(',')
},
path: ObjectUtil.isEmpty(this.path) ? ["/"] : this.path.split(',')
} : undefined,
}
};
}
}
class KcpStreamSettings extends CommonClass {
constructor(mtu=1350, tti=20,
uplinkCapacity=5,
downlinkCapacity=20,
congestion=false,
readBufferSize=2,
writeBufferSize=2,
type='none',
seed='',
) {
super();
this.mtu = mtu;
this.tti = tti;
this.upCap = uplinkCapacity;
this.downCap = downlinkCapacity;
this.congestion = congestion;
this.readBuffer = readBufferSize;
this.writeBuffer = writeBufferSize;
this.type = type;
this.seed = seed;
}
static fromJson(json={}) {
return new KcpStreamSettings(
json.mtu,
json.tti,
json.uplinkCapacity,
json.downlinkCapacity,
json.congestion,
json.readBufferSize,
json.writeBufferSize,
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
json.seed,
);
}
toJson() {
return {
mtu: this.mtu,
tti: this.tti,
uplinkCapacity: this.upCap,
downlinkCapacity: this.downCap,
congestion: this.congestion,
readBufferSize: this.readBuffer,
writeBufferSize: this.writeBuffer,
header: {
type: this.type,
},
seed: this.seed,
};
}
}
class WsStreamSettings extends CommonClass {
constructor(path='/', host='') {
super();
this.path = path;
this.host = host;
}
static fromJson(json={}) {
return new WsStreamSettings(
json.path,
json.headers && !ObjectUtil.isEmpty(json.headers.Host) ? json.headers.Host : '',
);
}
toJson() {
return {
path: this.path,
headers: ObjectUtil.isEmpty(this.host) ? undefined : {Host: this.host},
};
}
}
class HttpStreamSettings extends CommonClass {
constructor(path='/', host='') {
super();
this.path = path;
this.host = host;
}
static fromJson(json={}) {
return new HttpStreamSettings(
json.path,
json.host ? json.host.join(',') : '',
);
}
toJson() {
return {
path: this.path,
host: ObjectUtil.isEmpty(this.host) ? [''] : this.host.split(','),
}
}
}
class QuicStreamSettings extends CommonClass {
constructor(security='none',
key='', type='none') {
super();
this.security = security;
this.key = key;
this.type = type;
}
static fromJson(json={}) {
return new QuicStreamSettings(
json.security,
json.key,
json.header ? json.header.type : 'none',
);
}
toJson() {
return {
security: this.security,
key: this.key,
header: {
type: this.type,
}
}
}
}
class GrpcStreamSettings extends CommonClass {
constructor(serviceName="", multiMode=false) {
super();
this.serviceName = serviceName;
this.multiMode = multiMode;
}
static fromJson(json={}) {
return new GrpcStreamSettings(json.serviceName, json.multiMode);
}
toJson() {
return {
serviceName: this.serviceName,
multiMode: this.multiMode,
}
}
}
class TlsStreamSettings extends CommonClass {
constructor(serverName='',
alpn=[],
fingerprint = '',
allowInsecure = false) {
super();
this.serverName = serverName;
this.alpn = alpn;
this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure;
}
static fromJson(json={}) {
return new TlsStreamSettings(
json.serverName,
json.alpn,
json.fingerprint,
json.allowInsecure,
);
}
toJson() {
return {
serverName: this.serverName,
alpn: this.alpn,
fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure,
};
}
}
class RealityStreamSettings extends CommonClass {
constructor(publicKey = '', fingerprint = '', serverName = '', shortId = '', spiderX = '/') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.shortId = shortId
this.spiderX = spiderX;
}
static fromJson(json = {}) {
return new RealityStreamSettings(
json.publicKey,
json.fingerprint,
json.serverName,
json.shortId,
json.spiderX,
);
}
toJson() {
return {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
shortId: this.shortId,
spiderX: this.spiderX,
};
}
};
class StreamSettings extends CommonClass {
constructor(network='tcp',
security='none',
tlsSettings=new TlsStreamSettings(),
realitySettings = new RealityStreamSettings(),
tcpSettings=new TcpStreamSettings(),
kcpSettings=new KcpStreamSettings(),
wsSettings=new WsStreamSettings(),
httpSettings=new HttpStreamSettings(),
quicSettings=new QuicStreamSettings(),
grpcSettings=new GrpcStreamSettings(),
) {
super();
this.network = network;
this.security = security;
this.tls = tlsSettings;
this.reality = realitySettings;
this.tcp = tcpSettings;
this.kcp = kcpSettings;
this.ws = wsSettings;
this.http = httpSettings;
this.quic = quicSettings;
this.grpc = grpcSettings;
}
get isTls() {
return this.security === 'tls';
}
get isReality() {
return this.security === "reality";
}
static fromJson(json={}) {
return new StreamSettings(
json.network,
json.security,
TlsStreamSettings.fromJson(json.tlsSettings),
RealityStreamSettings.fromJson(json.realitySettings),
TcpStreamSettings.fromJson(json.tcpSettings),
KcpStreamSettings.fromJson(json.kcpSettings),
WsStreamSettings.fromJson(json.wsSettings),
HttpStreamSettings.fromJson(json.httpSettings),
QuicStreamSettings.fromJson(json.quicSettings),
GrpcStreamSettings.fromJson(json.grpcSettings),
);
}
toJson() {
const network = this.network;
return {
network: network,
security: this.security,
tlsSettings: this.security == 'tls' ? this.tls.toJson() : undefined,
realitySettings: this.security == 'reality' ? this.reality.toJson() : undefined,
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
httpSettings: network === 'http' ? this.http.toJson() : undefined,
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
};
}
}
class Outbound extends CommonClass {
constructor(
tag='',
protocol=Protocols.VMess,
settings=null,
streamSettings = new StreamSettings(),
) {
super();
this.tag = tag;
this._protocol = protocol;
this.settings = settings == null ? Outbound.Settings.getSettings(protocol) : settings;
this.stream = streamSettings;
}
get protocol() {
return this._protocol;
}
set protocol(protocol) {
this._protocol = protocol;
this.settings = Outbound.Settings.getSettings(protocol);
this.stream = new StreamSettings();
}
canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network);
}
//this is used for xtls-rprx-vision
canEnableTlsFlow() {
if ((this.stream.security != 'none') && (this.stream.network === "tcp")) {
return this.protocol === Protocols.VLESS;
}
return false;
}
canEnableReality() {
if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "http", "grpc"].includes(this.stream.network);
}
canEnableStream() {
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
}
hasVnext() {
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
}
hasServers() {
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);
}
hasAddressPort() {
return [
Protocols.DNS,
Protocols.VMess,
Protocols.VLESS,
Protocols.Trojan,
Protocols.Shadowsocks,
Protocols.Socks,
Protocols.HTTP
].includes(this.protocol);
}
hasUsername() {
return [Protocols.Socks, Protocols.HTTP].includes(this.protocol);
}
static fromJson(json={}) {
return new Outbound(
json.tag,
json.protocol,
Outbound.Settings.fromJson(json.protocol, json.settings),
StreamSettings.fromJson(json.streamSettings),
)
}
toJson() {
return {
tag: this.tag == '' ? undefined : this.tag,
protocol: this.protocol,
settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
streamSettings: this.canEnableStream() ? this.stream.toJson() : undefined,
};
}
}
Outbound.Settings = class extends CommonClass {
constructor(protocol) {
super();
this.protocol = protocol;
}
static getSettings(protocol) {
switch (protocol) {
case Protocols.Freedom: return new Outbound.FreedomSettings();
case Protocols.Blackhole: return new Outbound.BlackholeSettings();
case Protocols.DNS: return new Outbound.DNSSettings();
case Protocols.VMess: return new Outbound.VmessSettings();
case Protocols.VLESS: return new Outbound.VLESSSettings();
case Protocols.Trojan: return new Outbound.TrojanSettings();
case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings();
case Protocols.Socks: return new Outbound.SocksSettings();
case Protocols.HTTP: return new Outbound.HttpSettings();
default: return null;
}
}
static fromJson(protocol, json) {
switch (protocol) {
case Protocols.Freedom: return Outbound.FreedomSettings.fromJson(json);
case Protocols.Blackhole: return Outbound.BlackholeSettings.fromJson(json);
case Protocols.DNS: return Outbound.DNSSettings.fromJson(json);
case Protocols.VMess: return Outbound.VmessSettings.fromJson(json);
case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json);
case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json);
case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json);
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
default: return null;
}
}
toJson() {
return {};
}
};
Outbound.FreedomSettings = class extends CommonClass {
constructor(domainStrategy='', fragment={}) {
super();
this.domainStrategy = domainStrategy;
this.fragment = fragment;
}
static fromJson(json={}) {
return new Outbound.FreedomSettings(
json.domainStrategy,
json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : undefined,
);
}
toJson() {
return {
domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,
fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
};
}
};
Outbound.FreedomSettings.Fragment = class extends CommonClass {
constructor(packets='1-3',length='',interval=''){
super();
this.packets = packets;
this.length = length;
this.interval = interval;
}
static fromJson(json={}) {
return new Outbound.FreedomSettings.Fragment(
json.packets,
json.length,
json.interval,
);
}
};
Outbound.BlackholeSettings = class extends CommonClass {
constructor(type) {
super();
this.type;
}
static fromJson(json={}) {
return new Outbound.BlackholeSettings(
json.response ? json.response.type : undefined,
);
}
toJson() {
return {
response: ObjectUtil.isEmpty(this.type) ? undefined : {type: this.type},
};
}
};
Outbound.DNSSettings = class extends CommonClass {
constructor(network='udp', address='1.1.1.1', port=53) {
super();
this.network = network;
this.address = address;
this.port = port;
}
static fromJson(json={}){
return new Outbound.DNSSettings(
json.network,
json.address,
json.port,
);
}
};
Outbound.VmessSettings = class extends CommonClass {
constructor(address, port, id) {
super();
this.address = address;
this.port = port;
this.id = id;
}
static fromJson(json={}) {
if(ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
return new Outbound.VmessSettings(
json.vnext[0].address,
json.vnext[0].port,
json.vnext[0].users[0].id,
);
}
toJson() {
return {
vnext: [{
address: this.address,
port: this.port,
users: [{id: this.id}],
}],
};
}
};
Outbound.VLESSSettings = class extends CommonClass {
constructor(address, port, id, flow) {
super();
this.address = address;
this.port = port;
this.id = id;
this.flow = flow;
}
static fromJson(json={}) {
if(ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
return new Outbound.VLESSSettings(
json.vnext[0].address,
json.vnext[0].port,
json.vnext[0].users[0].id,
json.vnext[0].users[0].flow,
);
}
toJson() {
return {
vnext: [{
address: this.address,
port: this.port,
users: [{id: this.id, flow: this.flow}],
}],
};
}
};
Outbound.TrojanSettings = class extends CommonClass {
constructor(address, port, password) {
super();
this.address = address;
this.port = port;
this.password = password;
}
static fromJson(json={}) {
if(ObjectUtil.isArrEmpty(json.servers)) return new Outbound.TrojanSettings();
return new Outbound.TrojanSettings(
json.servers[0].address,
json.servers[0].port,
json.servers[0].password,
);
}
toJson() {
return {
servers: [{
address: this.address,
port: this.port,
password: this.password,
}],
};
}
};
Outbound.ShadowsocksSettings = class extends CommonClass {
constructor(address, port, password, method, uot) {
super();
this.address = address;
this.port = port;
this.password = password;
this.method = method;
this.uot = uot;
}
static fromJson(json={}) {
servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{}];
return new Outbound.ShadowsocksSettings(
servers[0].address,
servers[0].port,
servers[0].password,
servers[0].method,
servers[0].uot,
);
}
toJson() {
return {
servers: [{
address: this.address,
port: this.port,
password: this.password,
method: this.method,
uot: this.uot,
}],
};
}
};
Outbound.SocksSettings = class extends CommonClass {
constructor(address, port, user, password) {
super();
this.address = address;
this.port = port;
this.user = user;
this.password = password;
}
static fromJson(json={}) {
servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
return new Outbound.SocksSettings(
servers[0].address,
servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
ObjectUtil.isArrEmpty(servers[0].password) ? '' : servers[0].users[0].password,
);
}
toJson() {
return {
servers: [{
address: this.address,
port: this.port,
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, password: this.password}],
}],
};
}
};
Outbound.HttpSettings = class extends CommonClass {
constructor(address, port, user, password) {
super();
this.address = address;
this.port = port;
this.user = user;
this.password = password;
}
static fromJson(json={}) {
servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
return new Outbound.HttpSettings(
servers[0].address,
servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
ObjectUtil.isArrEmpty(servers[0].password) ? '' : servers[0].users[0].password,
);
}
toJson() {
return {
servers: [{
address: this.address,
port: this.port,
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, password: this.password}],
}],
};
}
};

View File

@@ -0,0 +1,533 @@
{{define "form/outbound"}}
<!-- base -->
<a-form layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "protocol" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.protocol" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.outbound.tag" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.tag" style="width: 250px" @change="check" :style="duplicateTag? 'border-color: red;' : ''"></a-input>
</a-form-item>
</td>
</tr>
<!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom">
<tr>
<td>Strategy</td>
<td>
<a-form-item>
<a-select
v-model="outbound.settings.domainStrategy"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Fragment</td>
<td>
<a-form-item>
<a-switch
:checked="Object.keys(outbound.settings.fragment).length >0"
@change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}">
</a-switch>
</a-form-item>
</td>
</tr>
<template v-if="Object.keys(outbound.settings.fragment).length >0">
<tr>
<td>Packets</td>
<td>
<a-form-item>
<a-select
v-model="outbound.settings.fragment.packets"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Length</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.fragment.length" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Interval</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.fragment.interval" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole">
<tr>
<td>Response Type</td>
<td>
<a-form-item>
<a-select
v-model="outbound.settings.type"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
<!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS">
<tr>
<td>{{ i18n "pages.inbounds.network" }}</td>
<td>
<a-form-item>
<a-select
v-model="outbound.settings.network"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
<!-- Address + Port -->
<template v-if="outbound.hasAddressPort()">
<tr>
<td>{{ i18n "pages.inbounds.address" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.address" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.settings.port"></a-input-number>
</a-form-item>
</td>
</tr>
</template>
<!-- Vnext (vless/vmess) settings -->
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<tr>
<td>ID</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.id" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<!-- vless settings -->
<template v-if="outbound.canEnableTlsFlow()">
<tr>
<td>Flow</td>
<td>
<a-form-item>
<a-select v-model="outbound.settings.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<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>
</td>
</tr>
</template>
</template>
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
<template v-if="outbound.hasServers()">
<tr v-if="outbound.hasUsername()">
<td>{{ i18n "username" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.user" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.password" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<tr>
<td>{{ i18n "encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>UDP over TCP</td>
<td>
<a-form-item>
<a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- stream settings -->
<template v-if="outbound.canEnableStream()">
<tr>
<td>{{ i18n "transmission" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">KCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option>
<a-select-option value="http">HTTP2</a-select-option>
<a-select-option value="quic">QUIC</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<template v-if="outbound.stream.network === 'tcp'">
<tr>
<td>http {{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-switch
:checked="outbound.stream.tcp.type === 'http'"
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'">
</a-switch>
</a-form-item>
</td>
</tr>
<template v-if="outbound.stream.tcp.type == 'http'">
<tr>
<td>{{ i18n "host" }}</td>
<td>
<a-form-item>
<a-input style="width: 250px;" v-model.trim="outbound.stream.tcp.host"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
<td>
<a-form-item>
<a-input style="width: 250px;" v-model.trim="outbound.stream.tcp.path"></a-input>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- kcp -->
<template v-if="outbound.stream.network === 'kcp'">
<tr>
<td>{{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model="outbound.stream.kcp.seed" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>mtu</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.mtu"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>tti (ms)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.tti"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>uplink capacity (MB/S)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.upCap"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>downlink capacity (MB/S)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.downCap"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>congestion</td>
<td>
<a-form-item>
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>read buffer size (MB)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>write buffer size (MB)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item>
</td>
</tr>
</template>
<!-- ws -->
<template v-if="outbound.stream.network === 'ws'">
<tr>
<td>{{ i18n "host" }}</td>
<td><a-form-item><a-input style="width: 250px" v-model="outbound.stream.ws.host"></a-input></a-form-item></td>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
<td><a-form-item><a-input style="width: 250px;" v-model.trim="outbound.stream.ws.path"></a-input></a-form-item></td>
</tr>
</template>
<!-- http -->
<template v-if="outbound.stream.network === 'http'">
<tr>
<td>{{ i18n "host" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.http.host" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.http.path" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
</template>
<!-- quic -->
<template v-if="outbound.stream.network === 'quic'">
<tr>
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.quic.security" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.quic.key" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.quic.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
<!-- grpc -->
<template v-if="outbound.stream.network === 'grpc'">
<tr>
<td>serviceName</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.grpc.serviceName" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>MultiMode</td>
<td>
<a-form-item>
<a-switch v-model="outbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- tls settings -->
<template v-if="outbound.canEnableTls()">
<tr>
<td>{{ i18n "security" }}</td>
<td>
<a-form-item>
<a-radio-group v-model="outbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button>
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button>
</a-radio-group>
</a-form-item>
</td>
</tr>
<template v-if="outbound.stream.isTls">
<tr>
<td>SNI</td>
<td>
<a-form-item placeholder="Server Name Indication">
<a-input v-model.trim="outbound.stream.tls.serverName" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.tls.fingerprint"
style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<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>
</a-form-item>
</td>
</tr>
<tr>
<td>ALPN</td>
<td>
<a-form-item>
<a-select
mode="multiple"
style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="outbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Allow insecure</td>
<td>
<a-form-item>
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item>
</td>
</tr>
</template>
<!-- reality settings -->
<template v-if="outbound.stream.isReality">
<tr>
<td>{{ i18n "domainName" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.reality.serverName" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.reality.fingerprint"
style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Short Id</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.reality.shortId" style="width:250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>SpiderX</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.reality.spiderX" style="width:250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Public Key</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.reality.publicKey" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
</template>
</template>
</table>
</a-form>
{{end}}

View File

@@ -5,6 +5,8 @@
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
<script src="{{ .base_path }}assets/js/model/outbound.js"></script>
<script src="{{ .base_path }}assets/codemirror/codemirror.js"></script>
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
@@ -88,7 +90,7 @@
</a-col>
</a-row>
</a-card>
<a-tabs default-active-key="1" @change="(activeKey) => { if(activeKey == 'tpl-3') this.changeCode(); }">
<a-tabs default-active-key="1" @change="(activeKey) => { if(activeKey == 'tpl-4') this.changeCode(); }">
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.xray.basicTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
@@ -206,7 +208,7 @@
message='{{ i18n "pages.xray.RoutingsDesc"}}' show-icon></a-alert>
<a-button type="primary" icon="plus" @click="addRule">{{ i18n "pages.xray.rules.add" }}</a-button>
<a-table :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered
:row-key="rule => rule.key"
:row-key="r => r.key"
:data-source="routingRuleData"
:scroll="isMobile ? {} : { x: 1000 }"
:pagination="false"
@@ -309,7 +311,79 @@
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true"></a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
<a-button type="primary" icon="plus" @click="addOutbound()">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
<a-button type="primary" icon="plus" @click="addReverse()">{{ i18n "pages.xray.outbound.addReverse" }}</a-button>
<a-row>
<a-col :sm="24" :md="12">
<p style="margin: 10px;">{{ i18n "pages.xray.Outbounds"}}</p>
<a-table :columns="outboundColumns" bordered
:row-key="r => r.key"
:data-source="outboundData"
:scroll="isMobile ? {} : { x: 200 }"
:pagination="false"
:indent-size="0"
:style="isMobile ? 'padding: 5px 5px' : 'margin-right: 1px;'">
<template slot="action" slot-scope="text, outbound, index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editOutbound(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteOutbound(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="address" slot-scope="text, outbound, index">
<p style="margin: 0 5px;" v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
</template>
<template slot="protocol" slot-scope="text, outbound, index">
<a-tag style="margin:0;" color="purple">[[ outbound.protocol ]]</a-tag>
<template v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<a-tag style="margin:0;" color="blue">[[ outbound.streamSettings.network ]]</a-tag>
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='tls'" color="green">tls</a-tag>
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag>
</template>
</template>
</a-table>
</a-col>
<a-col :sm="24" :md="12" v-if="reverseData.length>0">
<p style="margin: 10px;">{{ i18n "pages.xray.outbound.reverse"}}</p>
<a-table :columns="reverseColumns" bordered
:row-key="r => r.key"
:data-source="reverseData"
:scroll="isMobile ? {} : { x: 200 }"
:pagination="false"
:indent-size="0"
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
<template slot="action" slot-scope="text, reverse, index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editReverse(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteReverse(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</a-table>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;" force-render="true">
<a-list-item-meta title='{{ i18n "pages.xray.Template"}}' description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''">
@@ -330,9 +404,11 @@
{{template "component/themeSwitcher" .}}
{{template "component/setting"}}
{{template "ruleModal"}}
{{template "outModal"}}
{{template "reverseModal"}}
<script>
const rulesColumns = [
{ title: "#", align: 'center', width: 10, scopedSlots: { customRender: 'action' } },
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.rules.source"}}', children: [
{ title: 'IP', dataIndex: "source", align: 'center', width: 20, ellipsis: true },
{ title: 'port', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true } ]},
@@ -351,10 +427,24 @@
];
const rulesMobileColumns = [
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.rules.inbound"}}', align: 'center', width: 30, ellipsis: true, scopedSlots: { customRender: 'inbound' } },
{ title: '{{ i18n "pages.xray.rules.outbound"}}', align: 'center', width: 30, ellipsis: true, scopedSlots: { customRender: 'outbound' } },
{ title: '{{ i18n "pages.xray.rules.info"}}', align: 'center', width: 20, ellipsis: true, scopedSlots: { customRender: 'info' } },
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.rules.inbound"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'inbound' } },
{ title: '{{ i18n "pages.xray.rules.outbound"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'outbound' } },
{ title: '{{ i18n "pages.xray.rules.info"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'info' } },
];
const outboundColumns = [
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
];
const reverseColumns = [
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.outbound.type"}}', dataIndex: 'type', align: 'center', width: 50 },
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
{ title: '{{ i18n "pages.xray.outbound.domain"}}', dataIndex: 'domain', align: 'center', width: 50 },
];
const app = new Vue({
@@ -405,7 +495,6 @@
tag: "direct",
protocol: "freedom"
},
outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
settingsData: {
protocols: {
@@ -589,6 +678,127 @@
}
return true;
},
findOutboundAddress(o) {
serverObj = null;
switch(o.protocol){
case Protocols.VMess:
case Protocols.VLESS:
serverObj = o.settings.vnext;
break;
case Protocols.HTTP:
case Protocols.Socks:
case Protocols.Shadowsocks:
case Protocols.Trojan:
serverObj = o.settings.servers;
break;
case Protocols.DNS:
return [o.settings.address + ':' + o.settings.port];
default:
return null;
}
return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : null;
},
addOutbound(){
outModal.show({
title: '{{ i18n "pages.xray.outbound.addOutbound"}}',
okText: '{{ i18n "pages.xray.outbound.addOutbound" }}',
confirm: (outbound) => {
outModal.loading();
if(outbound.tag.length > 0){
this.templateSettings.outbounds.push(outbound);
this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
}
outModal.close();
},
isEdit: false
});
},
editOutbound(index){
outModal.show({
title: '{{ i18n "pages.xray.outbound.editOutbound"}} ' + (index+1),
outbound: app.templateSettings.outbounds[index],
confirm: (outbound) => {
outModal.loading();
this.templateSettings.outbounds[index] = outbound;
this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
outModal.close();
},
isEdit: true
});
},
deleteOutbound(index){
outbounds = this.templateSettings.outbounds;
outbounds.splice(index,1);
this.outboundSettings = JSON.stringify(outbounds);
},
addReverse(){
reverseModal.show({
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
okText: '{{ i18n "pages.xray.outbound.addReverse" }}',
confirm: (reverse, rules) => {
reverseModal.loading();
if(reverse.tag.length > 0){
newTemplateSettings = this.templateSettings;
if(newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {};
if(newTemplateSettings.reverse[reverse.type+'s'] == undefined) newTemplateSettings.reverse[reverse.type+'s'] = [];
newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain });
this.templateSettings = newTemplateSettings;
// Add related rules
this.templateSettings.routing.rules.push(...rules);
this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
}
reverseModal.close();
},
isEdit: false
});
},
editReverse(index){
if(this.reverseData[index].type == "bridge") {
oldRules = this.templateSettings.routing.rules.filter(r => r.inboundTag && r.inboundTag[0] == this.reverseData[index].tag);
} else {
oldRules = this.templateSettings.routing.rules.filter(r => r.outboundTag && r.outboundTag == this.reverseData[index].tag);
}
reverseModal.show({
title: '{{ i18n "pages.xray.outbound.editReverse"}} ' + (index+1),
reverse: this.reverseData[index],
rules: oldRules,
confirm: (reverse, rules) => {
reverseModal.loading();
if(reverse.tag.length > 0){
oldtag = this.reverseData[index].tag;
this.deleteReverse(index);
newTemplateSettings = this.templateSettings;
if(newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {};
if(newTemplateSettings.reverse[reverse.type+'s'] == undefined) newTemplateSettings.reverse[reverse.type+'s'] = [];
newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain });
this.templateSettings = newTemplateSettings;
// Adjust Rules
newRules = this.templateSettings.routing.rules.filter(r => r.outboundTag != oldtag && (r.inboundTag && !r.inboundTag.includes(oldtag)));
newRules.push(...rules)
this.routingRuleSettings = JSON.stringify(newRules);
}
reverseModal.close();
},
isEdit: true
});
},
deleteReverse(index){
oldData = this.reverseData[index];
newTemplateSettings = this.templateSettings;
reverseTypeObj = newTemplateSettings.reverse[oldData.type+'s'];
realIndex = reverseTypeObj.findIndex(r => r.tag==oldData.tag && r.domain==oldData.domain);
newTemplateSettings.reverse[oldData.type+'s'].splice(realIndex,1);
if(reverseTypeObj.length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s');
if(Object.keys(newTemplateSettings.reverse).length === 0) Reflect.deleteProperty(newTemplateSettings, 'reverse');
newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldtag && (r.inboundTag && !r.inboundTag.includes(oldtag)));
newTemplateSettings.routing.rules = newRules;
this.templateSettings = newTemplateSettings;
},
addRule(){
ruleModal.show({
title: '{{ i18n "pages.xray.rules.add"}}',
@@ -662,6 +872,35 @@
this.templateSettings = newTemplateSettings;
},
},
outboundData: {
get: function () {
data = []
if (this.templateSettings != null) {
this.templateSettings.outbounds.forEach((o, index) => {
data.push({'key': index, ...o});
});
}
return data;
},
},
reverseData: {
get: function () {
data = []
if (this.templateSettings != null && this.templateSettings.reverse != null) {
if(this.templateSettings.reverse.bridges) {
this.templateSettings.reverse.bridges.forEach((o, index) => {
data.push({'key': index, 'type':'bridge', ...o});
});
}
if(this.templateSettings.reverse.portals){
this.templateSettings.reverse.portals.forEach((o, index) => {
data.push({'key': index, 'type':'portal', ...o});
});
}
}
return data;
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {

View File

@@ -0,0 +1,75 @@
{{define "outModal"}}
<a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok"
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-button-props="{ props: { disabled: !isValid } }"
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<pre>[[ outModal.outbound ]]</pre>
{{template "form/outbound"}}
</a-modal>
<script>
const outModal = {
title: '',
visible: false,
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
outbound: new Outbound(),
outboundTags: [],
ok() {
ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());
},
show({ title='', okText='{{ i18n "sure" }}', outbound, confirm=(outbound)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
this.outbound = isEdit ? Outbound.fromJson(outbound) : new Outbound();
this.isEdit = isEdit;
this.outboundTags = app.templateSettings.outbounds.map(obj => obj.tag);
},
close() {
outModal.visible = false;
outModal.loading(false);
},
loading(loading) {
outModal.confirmLoading = loading;
}
};
new Vue({
delimiters: ['[[', ']]'],
el: '#out-modal',
data: {
outModal: outModal,
duplicateTag: false,
isValid: false,
get outbound() {
return outModal.outbound;
},
},
methods: {
streamNetworkChange() {
if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound.canEnableTlsFlow()) {
delete this.outModal.outbound.settings.flow;
}
},
canEnableTls() {
return this.outModal.outbound.canEnableTls();
},
check(){
if(outModal.outbound.tag == '' || this.outModal.outboundTags.includes(outModal.outbound.tag)){
this.duplicateTag = true;
this.isValid = false;
} else {
this.duplicateTag = false;
this.isValid = true;
}
},
},
});
</script>
{{end}}

View File

@@ -0,0 +1,174 @@
{{define "reverseModal"}}
<a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.xray.outbound.type" }}</td>
<td>
<a-form-item>
<a-select v-model="reverseModal.reverse.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in reverseTypes" :value="y">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.outbound.tag" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="reverseModal.reverse.tag" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.outbound.domain" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="reverseModal.reverse.domain" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<template v-if="reverseModal.reverse.type=='bridge'">
<tr>
<td>{{ i18n "pages.xray.outbound.intercon" }}</td>
<td>
<a-form-item>
<a-select v-model="reverseModal.rules[0].outboundTag" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.rules.outbound" }}</td>
<td>
<a-form-item>
<a-select v-model="reverseModal.rules[1].outboundTag" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
<template v-else>
<tr>
<td>{{ i18n "pages.xray.outbound.intercon" }}</td>
<td>
<a-form-item>
<a-checkbox-group
v-model="reverseModal.rules[0].inboundTag"
:options="reverseModal.inboundTags"></a-checkbox-group>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.rules.outbound" }}</td>
<td>
<a-form-item>
<a-checkbox-group
v-model="reverseModal.rules[1].inboundTag"
:options="reverseModal.inboundTags"></a-checkbox-group>
</a-form-item>
</td>
</tr>
</template>
</table>
</a-form>
</a-modal>
<script>
const reverseModal = {
title: '',
visible: false,
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
reverse: {
tag: "",
type: "",
domain: ""
},
rules: [
{ outboundTag: '', inboundTag: []},
{ outboundTag: '', inboundTag: []}
],
inboundTags: [],
outboundTags: [],
ok() {
reverseModal.rules[0].domain = ["full:" + reverseModal.reverse.domain];
reverseModal.rules[0].type = 'field';
reverseModal.rules[1].type = 'field';
if(reverseModal.reverse.type == 'bridge'){
reverseModal.rules[0].inboundTag.push(reverseModal.reverse.tag);
reverseModal.rules[1].inboundTag.push(reverseModal.reverse.tag);
} else {
reverseModal.rules[0].outboundTag = reverseModal.reverse.tag;
reverseModal.rules[1].outboundTag = reverseModal.reverse.tag;
}
ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules);
},
show({ title='', okText='{{ i18n "sure" }}', reverse, rules, confirm=(reverse, rules)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
this.reverse = {
tag: reverse.tag,
type: reverse.type,
domain: reverse.domain,
};
reverse;
rules0 = rules.filter(r => r.domain != null);
rules1 = rules.filter(r => r.domain == null);
this.rules = [];
this.rules.push({
domain: rules0[0].domain,
outboundTag: rules0[0].outboundTag,
inboundTag: rules0.map(r => r.inboundTag).flat()
});
this.rules.push({
outboundTag: rules1[0].outboundTag,
inboundTag: rules1.map(r => r.inboundTag).flat()
});
} else {
this.reverse = {
tag: "reverse-" + app.reverseData.length,
type: "bridge",
domain: "reverse.xui"
}
this.rules = [
{ outboundTag: '', inboundTag: []},
{ outboundTag: '', inboundTag: []}
]
}
this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags);
this.outboundTags = app.templateSettings.outbounds.map(obj => obj.tag);
},
close() {
reverseModal.visible = false;
reverseModal.loading(false);
},
loading(loading) {
reverseModal.confirmLoading = loading;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#reverse-modal',
data: {
reverseModal: reverseModal,
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
},
methods: {
}
});
</script>
{{end}}

View File

@@ -217,6 +217,12 @@
this.inboundTags = app.templateSettings.inbounds.map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags);
this.outboundTags = app.templateSettings.outbounds.map(obj => obj.tag);
if(app.templateSettings.reverse){
if(app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
}
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
}
},
close() {
ruleModal.visible = false;
@@ -262,11 +268,7 @@
el: '#rule-modal',
data: {
ruleModal: ruleModal,
},
methods: {
},
computed: {
},
}
});
</script>