diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js new file mode 100644 index 00000000..505393d7 --- /dev/null +++ b/web/assets/js/model/outbound.js @@ -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}], + }], + }; + } +}; \ No newline at end of file diff --git a/web/html/xui/form/outbound.html b/web/html/xui/form/outbound.html new file mode 100644 index 00000000..46dceec5 --- /dev/null +++ b/web/html/xui/form/outbound.html @@ -0,0 +1,533 @@ +{{define "form/outbound"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ i18n "protocol" }} + + + [[ y ]] + + +
{{ i18n "pages.xray.outbound.tag" }} + + + +
+
+{{end}} \ No newline at end of file diff --git a/web/html/xui/xray.html b/web/html/xui/xray.html index 39990922..98ace054 100644 --- a/web/html/xui/xray.html +++ b/web/html/xui/xray.html @@ -5,6 +5,8 @@ + + @@ -88,7 +90,7 @@ - + @@ -206,7 +208,7 @@ message='{{ i18n "pages.xray.RoutingsDesc"}}' show-icon> {{ i18n "pages.xray.rules.add" }} - + + {{ i18n "pages.xray.outbound.addOutbound" }} + {{ i18n "pages.xray.outbound.addReverse" }} + + +

{{ i18n "pages.xray.Outbounds"}}

+ + + + + +
+ +

{{ i18n "pages.xray.outbound.reverse"}}

+ + + +
+
+
@@ -330,9 +404,11 @@ {{template "component/themeSwitcher" .}} {{template "component/setting"}} {{template "ruleModal"}} +{{template "outModal"}} +{{template "reverseModal"}} +{{end}} diff --git a/web/html/xui/xray_reverse_modal.html b/web/html/xui/xray_reverse_modal.html new file mode 100644 index 00000000..1af232cd --- /dev/null +++ b/web/html/xui/xray_reverse_modal.html @@ -0,0 +1,174 @@ +{{define "reverseModal"}} + + + + + + + + + + + + + + + + + +
{{ i18n "pages.xray.outbound.type" }} + + + [[ x ]] + + +
{{ i18n "pages.xray.outbound.tag" }} + + + +
{{ i18n "pages.xray.outbound.domain" }} + + + +
+
+
+ +{{end}} diff --git a/web/html/xui/xray_rule_modal.html b/web/html/xui/xray_rule_modal.html index 6f136f25..ebc17f98 100644 --- a/web/html/xui/xray_rule_modal.html +++ b/web/html/xui/xray_rule_modal.html @@ -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: { - }, + } });