all adjustments

This commit is contained in:
Alireza Ahmadi
2025-01-03 23:32:03 +01:00
parent ed48cdca33
commit fe428ed412
62 changed files with 2352 additions and 1975 deletions
+360 -350
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -14,6 +14,8 @@
:label="$t('in.port')"
hide-details
type="number"
min="1"
max="65535"
required
v-model.number="inbound.listen_port"></v-text-field>
</v-col>
+14 -14
View File
@@ -6,20 +6,20 @@
hide-details
:items="['4','4a','5']"
:label="$t('version')"
v-model="inData.outJson.version">
v-model="inData.out_json.version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needNetwork">
<Network :data="inData.outJson" />
<Network :data="inData.out_json" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needUot">
<UoT :data="inData.outJson" />
<UoT :data="inData.out_json" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.HTTP">
<v-text-field
:label="$t('transport.path')"
hide-details
v-model="inData.outJson.path">
v-model="inData.out_json.path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.VMess || type == inTypes.VLESS">
@@ -36,14 +36,14 @@
hide-details
:label="$t('types.vmess.security')"
:items="vmessSecurities"
v-model="inData.outJson.security">
v-model="inData.out_json.security">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.outJson.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
<v-switch v-model="inData.out_json.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.outJson.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
<v-switch v-model="inData.out_json.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
</v-col>
</template>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.Hysteria">
@@ -52,7 +52,7 @@
hide-details
type="number"
min="0"
v-model.number="inData.outJson.recv_window">
v-model.number="inData.out_json.recv_window">
</v-text-field>
</v-col>
<template v-if="type == inTypes.TUIC">
@@ -62,16 +62,16 @@
label="UDP Relay Mode"
:items="['native', 'quic']"
clearable
@click:clear="delete inData.outJson.udp_relay_mode"
v-model="inData.outJson.udp_relay_mode">
@click:clear="delete inData.out_json.udp_relay_mode"
v-model="inData.out_json.udp_relay_mode">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="UDP Over Stream" v-model="inData.outJson.udp_over_stream" hide-details></v-switch>
<v-switch color="primary" label="UDP Over Stream" v-model="inData.out_json.udp_over_stream" hide-details></v-switch>
</v-col>
</template>
</v-row>
<Headers :data="inData.outJson" v-if="type == inTypes.HTTP" />
<Headers :data="inData.out_json" v-if="type == inTypes.HTTP" />
</v-card>
</template>
@@ -114,8 +114,8 @@ export default {
needNetwork():boolean { return this.haveNetwork.includes(this.$props.type) },
needUot():boolean { return this.havUoT.includes(this.$props.type) },
packet_encoding: {
get() { return this.$props.inData.outJson.packet_encoding != undefined ? this.$props.inData.outJson.packet_encoding : 'none'; },
set(v:string) { this.$props.inData.outJson.packet_encoding = v != "none" ? v : undefined }
get() { return this.$props.inData.out_json.packet_encoding != undefined ? this.$props.inData.out_json.packet_encoding : 'none'; },
set(v:string) { this.$props.inData.out_json.packet_encoding = v != "none" ? v : undefined }
},
},
components: { Network, UoT, Headers }
+21 -2
View File
@@ -4,7 +4,7 @@
<v-text-field
:label="$t('out.addr')"
hide-details
v-model="data.server">
v-model="address">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
@@ -13,7 +13,16 @@
type="number"
min="0"
hide-details
v-model="data.server_port">
v-model="port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="KeepAlive"
type="number"
min="0"
hide-details
v-model="data.persistent_keepalive_interval">
</v-text-field>
</v-col>
</v-row>
@@ -36,6 +45,8 @@
</template>
<script lang="ts">
import { KeepAlive } from 'vue';
export default {
props: ['data'],
data() {
@@ -54,6 +65,14 @@ export default {
}
}
},
address: {
get() { return this.$props.data.address },
set(v:string) { this.$props.data.address = v.length > 0 ? v : undefined }
},
port: {
get() { return this.$props.data.port },
set(v:number) { this.$props.data.port = v > 0 ? v : undefined }
}
}
}
</script>
+54 -45
View File
@@ -2,22 +2,40 @@
<v-card subtitle="Wireguard">
<v-row>
<v-col cols="12" sm="8">
<v-text-field v-model="data.private_key" :label="$t('types.wg.privKey')" hide-details></v-text-field>
<v-text-field
v-model="data.private_key"
:label="$t('types.wg.privKey')"
append-icon="mdi-key-star"
@click:append="newKey()"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="8">
<v-text-field v-model="data.peer_public_key" :label="$t('types.wg.pubKey')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8" v-if="data.pre_shared_key != undefined">
<v-text-field v-model="data.pre_shared_key" :label="$t('types.wg.psk')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8">
<v-text-field v-model="local_ips" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
<v-text-field v-model="address" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.reserved != undefined">
<v-text-field v-model="reserved" :label="'Reserved ' + $t('commaSeparated')" hide-details></v-text-field>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('in.port')"
hide-details
type="number"
min=1
v-model.number="data.listen_port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.udp_timeout != undefined">
<v-text-field
label="UDP Timeout"
hide-details
type="number"
min=0
:suffix="$t('date.m')"
v-model.number="udp_timeout">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.workers != undefined">
<v-text-field
:label="$t('types.wg.worker')"
@@ -39,24 +57,16 @@
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
<v-switch v-model="data.system" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.interface_name != undefined">
<v-col cols="12" sm="6" md="4" v-if="data.name != undefined">
<v-text-field
:label="$t('types.wg.ifName')"
hide-details
v-model.number="data.interface_name">
v-model="data.name">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.system_interface" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.gso" color="primary" :label="$t('types.wg.gso')" hide-details></v-switch>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
@@ -66,10 +76,7 @@
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionPsk" color="primary" :label="$t('types.wg.psk')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionRsrv" color="primary" label="Reserved" hide-details></v-switch>
<v-switch v-model="optionUdp" color="primary" label="UDP Timeout" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionWorker" color="primary" :label="$t('types.wg.worker')" hide-details></v-switch>
@@ -80,9 +87,6 @@
<v-list-item>
<v-switch v-model="optionInterface" color="primary" :label="$t('types.wg.ifName')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionPeers" color="primary" :label="$t('types.wg.multiPeer')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
@@ -95,7 +99,7 @@
<template v-for="(p, index) in data.peers">
<v-card style="margin-top: 1rem;">
<v-card-subtitle>
{{ $t('types.wg.peer') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="data.peers.splice(index,1)" />
{{ $t('types.wg.peer') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="data.peers.splice(index,1)" v-if="data.peers.length > 1" />
</v-card-subtitle>
<Peer :data="p" />
</v-card>
@@ -104,9 +108,8 @@
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
import Peer from '@/components/WgPeer.vue'
import { WgPeer } from '@/types/outbounds'
import WgUtil from '@/plugins/wgUtil'
export default {
props: ['data'],
@@ -117,13 +120,19 @@ export default {
},
methods: {
addPeer() {
this.$props.data.peers.push({server: '', port: ''})
this.$props.data.peers.push({
address: '',
port: this.$props.data.listen_port
})
},
newKey() {
this.$props.data.private_key = WgUtil.generateKeypair().privateKey
}
},
computed: {
optionPsk: {
get(): boolean { return this.$props.data.pre_shared_key != undefined },
set(v:boolean) { this.$props.data.pre_shared_key = v ? "" : undefined }
optionUdp: {
get(): boolean { return this.$props.data.udp_timeout != undefined },
set(v:boolean) { this.$props.data.udp_timeout = v ? "5m" : undefined }
},
optionRsrv: {
get(): boolean { return this.$props.data.reserved != undefined },
@@ -138,16 +147,12 @@ export default {
set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined }
},
optionInterface: {
get(): boolean { return this.$props.data.interface_name != undefined },
set(v:boolean) { this.$props.data.interface_name = v ? "" : undefined }
get(): boolean { return this.$props.data.name != undefined },
set(v:boolean) { this.$props.data.name = v ? "" : undefined }
},
optionPeers: {
get(): boolean { return this.$props.data.peers != undefined },
set(v:boolean) { this.$props.data.peers = v ? <WgPeer[]>[] : undefined }
},
local_ips: {
get() { return this.$props.data.local_address?.join(',') },
set(v:string) { this.$props.data.local_address = v.length > 0 ? v.split(',') : undefined }
address: {
get() { return this.$props.data.address?.join(',') },
set(v:string) { this.$props.data.address = v.length > 0 ? v.split(',') : undefined }
},
reserved: {
get() { return this.$props.data.reserved?.join(',') },
@@ -157,7 +162,11 @@ export default {
}
}
},
udp_timeout: {
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
set(v:number) { this.$props.data.udp_timeout = v > 0 ? v + 'm' : '5m' }
}
},
components: { Network, Peer }
components: { Peer }
}
</script>
+3 -220
View File
@@ -1,242 +1,25 @@
<template>
<v-card :subtitle="$t('objects.tls')">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tlsOptional">
<v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.enabled">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('template')"
:items="tlsItems"
@update:model-value="changeTlsItem($event)"
v-model="tlsId">
v-model="inbound.tls_id">
</v-select>
</v-col>
</v-row>
<template v-if="tls.enabled && tlsId == 0">
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="usePath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="tls.key=undefined; tls.certificate=undefined"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="tls.key_path=undefined; tls.certificate_path=undefined"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row v-if="usePath == 0">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.certPath')"
hide-details
v-model="tls.certificate_path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.keyPath')"
hide-details
v-model="tls.key_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.cert')"
hide-details
v-model="certText">
</v-textarea>
</v-col>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.key')"
hide-details
v-model="keyText">
</v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tls.server_name != undefined">
<v-text-field
label="SNI"
hide-details
v-model="tls.server_name">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.alpn">
<v-select
hide-details
label="ALPN"
multiple
:items="alpn"
v-model="tls.alpn">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tls.min_version">
<v-select
hide-details
:label="$t('tls.minVer')"
:items="tlsVersions"
v-model="tls.min_version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.max_version">
<v-select
hide-details
:label="$t('tls.maxVer')"
:items="tlsVersions"
v-model="tls.max_version">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="8" v-if="tls.cipher_suites != undefined">
<v-select
hide-details
:label="$t('tls.cs')"
multiple
:items="cipher_suites"
v-model="tls.cipher_suites">
</v-select>
</v-col>
</v-row>
</template>
<v-card-actions v-if="tls.enabled && tlsId == 0">
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMinV" color="primary" :label="$t('tls.minVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMaxV" color="primary" :label="$t('tls.maxVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import { i18n } from '@/locales'
import { iTls, defaultInTls } from '@/types/inTls'
export default {
props: ['inbound', 'tlsConfigs', 'tls_id'],
data() {
return {
menu: false,
usePath: this.$props.inbound.tls.key == undefined ? 0 : 1,
defaults: defaultInTls,
alpn: [
{ title: "H3", value: 'h3' },
{ title: "H2", value: 'h2' },
{ title: "Http/1.1", value: 'http/1.1' },
],
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
cipher_suites: [
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" },
{ title: "RSA-AES256-CBC-SHA", value: "TLS_RSA_WITH_AES_256_CBC_SHA" },
{ title: "RSA-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
{ title: "RSA-AES256-GCM-SHA384", value: "TLS_RSA_WITH_AES_256_GCM_SHA384" },
{ title: "AES128-GCM-SHA256", value: "TLS_AES_128_GCM_SHA256" },
{ title: "AES256-GCM-SHA384", value: "TLS_AES_256_GCM_SHA384" },
{ title: "CHACHA20-POLY1305-SHA256", value: "TLS_CHACHA20_POLY1305_SHA256" },
{ title: "ECDHE-ECDSA-AES128-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
{ title: "ECDHE-ECDSA-AES256-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
{ title: "ECDHE-RSA-AES128-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
{ title: "ECDHE-RSA-AES256-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
{ title: "ECDHE-ECDSA-AES128-GCM-SHA256", value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
{ title: "ECDHE-ECDSA-AES256-GCM-SHA384", value: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
{ title: "ECDHE-RSA-AES128-GCM-SHA256", value: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
{ title: "ECDHE-RSA-AES256-GCM-SHA384", value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
{ title: "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" },
{ title: "ECDHE-RSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" }
]
}
},
props: ['inbound', 'tlsConfigs'],
computed: {
tls(): iTls {
return <iTls> this.$props.inbound.tls
},
tlsItems(): any[] {
return [ { title: i18n.global.t('none'), value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
},
tlsId: {
get() { return this.tls_id.value?? 0 },
set(newValue: boolean) { this.$props.tls_id.value = newValue }
},
tlsEnable: {
get() { return this.tls.enabled?? false },
set(newValue: boolean) {
this.$props.inbound.tls = newValue ? { enabled: true } : {}
this.$props.tls_id.value = 0
}
},
tlsOptional(): boolean {
return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type)
},
certText: {
get(): string { return this.tls.certificate ? this.tls.certificate.join('\n') : '' },
set(newValue:string) { this.tls.certificate = newValue.split('\n') }
},
keyText: {
get(): string { return this.tls.key ? this.tls.key.join('\n') : '' },
set(newValue:string) { this.tls.key = newValue.split('\n') }
},
optionSNI: {
get(): boolean { return this.tls.server_name != undefined },
set(v:boolean) { this.tls.server_name = v ? '' : undefined }
},
optionALPN: {
get(): boolean { return this.tls.alpn != undefined },
set(v:boolean) { this.tls.alpn = v ? defaultInTls.alpn : undefined }
},
optionMinV: {
get(): boolean { return this.tls.min_version != undefined },
set(v:boolean) { this.tls.min_version = v ? defaultInTls.min_version : undefined }
},
optionMaxV: {
get(): boolean { return this.tls.max_version != undefined },
set(v:boolean) { this.tls.max_version = v ? defaultInTls.max_version : undefined }
},
optionCS: {
get(): boolean { return this.tls.cipher_suites != undefined },
set(v:boolean) { this.tls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
}
},
methods: {
changeTlsItem(id: number){
if (id>0) {
const tlsConfig = this.$props.tlsConfigs?.findLast((t:any) => t.id == id)
if (tlsConfig) this.$props.inbound.tls = tlsConfig.server
} else {
this.$props.inbound.tls = { enabled: this.tls.enabled }
}
}
}
}
+1 -22
View File
@@ -3,16 +3,13 @@
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
<span v-else style="width: 24px"></span>
<v-app-bar-title :text="$t(<string>route.name)" class="align-center text-center " />
<v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn>
<v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon>
</v-app-bar>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue"
import { ref } from "vue"
import { useTheme } from "vuetify"
import { FindDiff } from "@/plugins/utils"
import Data from "@/store/modules/data"
import { useRoute } from "vue-router";
defineProps(['isMobile'])
@@ -21,27 +18,9 @@ const route = useRoute();
const theme = useTheme()
const darkMode = ref(localStorage.getItem('theme') == "dark")
const store = Data()
const toggleTheme = () => {
darkMode.value = !darkMode.value
theme.global.name.value = darkMode.value ? "dark" : "light"
localStorage.setItem('theme', theme.global.name.value)
}
const saveChanges = () => {
store.pushData()
}
const oldData = computed((): any => {
return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs, inData: store.oldData.inData}
})
const newData = computed((): any => {
return {config: store.config, clients: store.clients, tls: store.tlsConfigs, inData: store.inData}
})
const stateChange = computed((): any => {
return !FindDiff.deepCompare(newData.value,oldData.value)
})
</script>
+1
View File
@@ -53,6 +53,7 @@ const menu = [
{ title: 'pages.inbounds', icon: 'mdi-cloud-download', path: '/inbounds' },
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
{ title: 'pages.endpoints', icon: 'mdi-cloud-tags', path: '/endpoints' },
{ title: 'pages.rules', icon: 'mdi-routes', path: '/rules' },
{ title: 'pages.tls', icon: 'mdi-certificate', path: '/tls' },
{ title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' },
+5 -11
View File
@@ -41,7 +41,7 @@
<DatePick :expiry="expDate" @submit="setDate" />
</v-col>
</v-row>
<v-row v-if="index != -1">
<v-row v-if="id > 0">
<v-col cols="12" sm="6" md="4" class="d-flex flex-column">
<div class="d-flex justify-space-between align-center">
<div>
@@ -80,11 +80,6 @@
></v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-switch v-model="clientStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-col>
</v-row>
</v-window-item>
<v-window-item value="t2">
<v-row v-for="(value, key) in clientConfig" :key="key">
@@ -189,7 +184,7 @@ import DatePick from '@/components/DateTime.vue'
import { HumanReadable } from '@/plugins/utils'
export default {
props: ['visible', 'data', 'index', 'inboundTags', 'groups', 'stats'],
props: ['visible', 'data', 'id', 'inboundTags', 'groups'],
emits: ['close', 'save'],
data() {
return {
@@ -206,7 +201,7 @@ export default {
},
methods: {
updateData() {
if (this.$props.index != -1) {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.client = createClient(newData)
this.title = "edit"
@@ -217,7 +212,6 @@ export default {
this.title = "add"
this.clientConfig = randomConfigs('client')
}
this.clientStats = this.$props.stats
this.links = this.client.links.filter(l => l.type == 'local')
this.extLinks = this.client.links.filter(l => l.type == 'external')
this.subLinks = this.client.links.filter(l => l.type == 'sub')
@@ -243,8 +237,8 @@ export default {
},
computed: {
clientInbounds: {
get() { return this.client.inbounds.length>0 ? this.client.inbounds.filter(i => this.inboundTags.includes(i)) : [] },
set(newValue:string[]) { this.client.inbounds = newValue.length == 0 ? [] : newValue }
get() { return this.client.inbounds.length>0 ? this.client.inbounds : [] },
set(v:any[]) { this.client.inbounds = v.length == 0 ? [] : v.map(i => i.value) }
},
expDate: {
get() { return this.client.expiry},
+1 -8
View File
@@ -59,11 +59,6 @@
></v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-switch v-model="bulkData.clientStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
@@ -109,7 +104,6 @@ export default {
clientInbounds: [],
expiry: 0,
Volume: 0,
clientStats: false,
},
patterns: [
{ title: i18n.global.t("bulk.random"), value: "random" },
@@ -129,7 +123,6 @@ export default {
clientInbounds: [],
expiry: 0,
Volume: 0,
clientStats: false,
}
},
closeModal() {
@@ -157,7 +150,7 @@ export default {
group: this.bulkData.group
}))
}
this.$emit('save', this.clients, this.bulkData.clientInbounds, this.bulkData.clientStats)
this.$emit('save', this.clients, this.bulkData.clientInbounds)
this.resetData() // reset to default
this.loading = false
},
+153
View File
@@ -0,0 +1,153 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.endpoint') }}
</v-card-title>
<v-divider></v-divider>
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;">
<v-tabs
v-model="tab"
align-tabs="center"
>
<v-tab value="t1">{{ $t('client.basics') }}</v-tab>
<v-tab value="t2">{{ $t('client.external') }}</v-tab>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="t1">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('type')"
:items="Object.keys(epTypes).map((key,index) => ({title: key, value: Object.values(epTypes)[index]}))"
v-model="endpoint.type"
@update:modelValue="changeType">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="endpoint.tag" :label="$t('objects.tag')" hide-details></v-text-field>
</v-col>
</v-row>
<Wireguard v-if="endpoint.type == epTypes.Wireguard" :data="endpoint" />
<Dial :dial="endpoint" :outTags="tags" />
</v-window-item>
<v-window-item value="t2">
<v-row>
<v-col cols="12">
<v-text-field v-model="link" :label="$t('client.external')" hide-details />
</v-col>
<v-col cols="12" align="center">
<v-btn hide-details variant="tonal" :loading="loading" @click="linkConvert">{{ $t('submit') }}</v-btn>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="text"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="text"
:loading="loading"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { EpTypes, createEndpoint } from '@/types/endpoints'
import RandomUtil from '@/plugins/randomUtil'
import Dial from '@/components/Dial.vue'
import Wireguard from '@/components/protocols/Wireguard.vue'
import HttpUtils from '@/plugins/httputil'
import WgUtil from '@/plugins/wgUtil'
export default {
props: ['visible', 'data', 'id', 'tags'],
emits: ['close', 'save'],
data() {
return {
endpoint: createEndpoint("wireguard",{ "tag": "" }),
title: "add",
tab: "t1",
link: "",
loading: false,
epTypes: EpTypes,
}
},
methods: {
updateData() {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.endpoint = createEndpoint(newData.type, newData)
this.title = "edit"
}
else {
const port = RandomUtil.randomIntRange(10000, 60000)
const randomIPoctet = RandomUtil.randomIntRange(1, 255)
this.endpoint = createEndpoint("wireguard",{
tag: "wireguard-" + RandomUtil.randomSeq(3),
address: ['10.0.0.'+ randomIPoctet.toString() +'/32','fe80::'+ randomIPoctet.toString(16) +'/128'],
listen_port: port,
private_key: WgUtil.generateKeypair().privateKey,
peers: [{
public_key: WgUtil.generateKeypair().publicKey,
allowed_ips: ['0.0.0.0/0', '::/0']
}]
})
this.title = "add"
}
this.tab = "t1"
},
changeType() {
// Tag change only in add endpoint
const tag = this.$props.id > 0 ? this.endpoint.tag : this.endpoint.type + "-" + RandomUtil.randomSeq(3)
// Use previous data
const prevConfig = { id: this.endpoint.id, tag: tag ,listen: this.endpoint.listen, listen_port: this.endpoint.listen_port }
this.endpoint = createEndpoint(this.endpoint.type, prevConfig)
},
closeModal() {
this.updateData() // reset
this.$emit('close')
},
saveChanges() {
this.loading = true
this.$emit('save', this.endpoint)
this.loading = false
},
async linkConvert() {
if (this.link.length>0){
this.loading = true
const msg = await HttpUtils.post('api/linkConvert', { link: this.link })
this.loading = false
if (msg.success) {
this.endpoint = msg.obj
this.tab = "t1"
this.link = ""
}
}
}
},
watch: {
visible(newValue) {
if (newValue) {
this.updateData()
}
},
},
components: { Dial, Wireguard }
}
</script>
+63 -42
View File
@@ -1,12 +1,18 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-dialog transition="dialog-bottom-transition" width="800" @after-enter="updateData">
<v-card class="rounded-lg" :loading="loading">
<v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.inbound') }}
</v-card-title>
<v-divider></v-divider>
<v-skeleton-loader
class="mx-auto border"
width="95%"
type="card, text, divider, list-item-two-line"
v-if="loading"
></v-skeleton-loader>
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;">
<v-container style="padding: 0;" :hidden="loading">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
@@ -45,19 +51,18 @@
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
<Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" />
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" />
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="tls_id" />
<InTls v-if="HasTls.includes(inbound.type)" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="inbound.tls_id" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" />
<v-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-window-item>
<v-window-item value="c">
<OutJsonVue :inData="inData" :type="inbound.type" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="out" :data="inData.outJson" />
<OutJsonVue :inData="inbound" :type="inbound.type" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="out" :data="inbound.out_json" />
<v-card>
<v-card-subtitle>{{ $t('in.multiDomain') }}
<v-icon @click="add_addr" icon="mdi-plus"></v-icon>
</v-card-subtitle>
<template v-for="addr,index in inData.addrs">
{{ $t('in.addr') }} #{{ (index+1) }} <v-icon icon="mdi-delete" @click="inData.addrs.splice(index,1)" />
<template v-for="addr,index in inbound.addrs">
{{ $t('in.addr') }} #{{ (index+1) }} <v-icon icon="mdi-delete" @click="inbound.addrs?.splice(index,1)" />
<v-divider></v-divider>
<AddrVue :addr="addr" :hasTls="Object.hasOwn(inbound,'tls')" />
</template>
@@ -79,6 +84,7 @@
color="blue-darken-1"
variant="text"
:loading="loading"
:disabled="!validate"
@click="saveChanges"
>
{{ $t('actions.save') }}
@@ -89,8 +95,7 @@
</template>
<script lang="ts">
import { InTypes, createInbound } from '@/types/inbounds'
import { Addr, InData } from '@/plugins/inData'
import { InTypes, createInbound, Addr } from '@/types/inbounds'
import RandomUtil from '@/plugins/randomUtil'
import Listen from '@/components/Listen.vue'
@@ -109,19 +114,17 @@ import Multiplex from '@/components/Multiplex.vue'
import Transport from '@/components/Transport.vue'
import AddrVue from '@/components/Addr.vue'
import OutJsonVue from '@/components/OutJson.vue'
import Data from '@/store/modules/data'
export default {
props: ['visible', 'data', 'cData', 'index', 'stats', 'inTags', 'outTags', 'tlsConfigs'],
props: ['visible', 'id', 'inTags', 'outTags', 'tlsConfigs'],
emits: ['close', 'save'],
data() {
return {
inbound: createInbound("direct",{ "tag": "" }),
inData: <InData>{},
inbound: createInbound("direct",{ id:0, "tag": "" }),
title: "add",
loading: false,
side: "s",
inTypes: InTypes,
inboundStats: false,
tls_id: { value: 0 },
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
HasInData: [
InTypes.SOCKS,
@@ -136,56 +139,65 @@ export default {
InTypes.TUIC,
InTypes.Hysteria2,
InTypes.Naive,
]
],
HasTls: [
InTypes.HTTP,
InTypes.VMess,
InTypes.Trojan,
InTypes.Naive,
InTypes.Hysteria,
InTypes.TUIC,
InTypes.Hysteria2,
InTypes.VLESS,
],
OnlyTLS: [InTypes.Hysteria, InTypes.Hysteria2, InTypes.TUIC, InTypes.Naive ],
}
},
methods: {
async loadData() {
this.loading = true
const inboundArray = await Data().loadInbounds([this.$props.id])
this.inbound = inboundArray[0]
this.loading = false
},
updateData() {
if (this.$props.index != -1) {
const newData = JSON.parse(this.$props.data)
this.inbound = createInbound(newData.type, newData)
this.tls_id.value = this.$props.tlsConfigs?.findLast((t:any) => t.inbounds?.includes(this.inbound.tag))?.id?? 0
if (this.HasInData.includes(this.inbound.type)){
this.inData = this.$props.cData?.length> 0 ? <InData>JSON.parse(this.$props.cData) : <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
} else {
this.inData = <InData>{id: -1}
}
if (this.$props.id > 0) {
this.loadData()
this.title = "edit"
}
else {
const port = RandomUtil.randomIntRange(10000, 60000)
this.inbound = createInbound("direct",{ tag: "direct-"+port ,listen: "::", listen_port: port })
this.tls_id.value = 0
this.inbound = createInbound("direct",{ id: 0, tag: "direct-"+port ,listen: "::", listen_port: port })
if (this.HasInData.includes(this.inbound.type)){
this.inData = <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
this.inbound.addrs = []
this.inbound.out_json = {}
} else {
this.inData = <InData>{id: -1}
delete this.inbound.addrs
delete this.inbound.out_json
}
this.title = "add"
this.loading = false
}
this.inboundStats = this.$props.stats
this.side = "s"
},
changeType() {
if (!this.inbound.listen_port) this.inbound.listen_port = RandomUtil.randomIntRange(10000, 60000)
// Tag change only in add inbound
const tag = this.$props.index != -1 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
const tag = this.$props.id > 0 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
// Use previous data
const prevConfig = { tag: tag ,listen: this.inbound.listen?? "::", listen_port: this.inbound.listen_port }
const prevConfig = { id: this.inbound.id, tag: tag ,listen: this.inbound.listen?? "::", listen_port: this.inbound.listen_port }
this.inbound = createInbound(this.inbound.type, this.inbound.type != this.inTypes.Tun ? prevConfig : { tag: tag })
if (this.HasInData.includes(this.inbound.type)){
if (this.inData.id == -1) this.inData.id = 0
this.inData.addrs = []
this.inData.outJson = {}
this.inData.tag = tag
this.inbound.addrs = []
this.inbound.out_json = {}
} else {
this.inData = <InData>{id: -1}
delete this.inbound.addrs
delete this.inbound.out_json
}
this.tls_id.value = 0
this.side = "s"
},
add_addr() {
this.inData.addrs.push(<Addr>{ server: location.hostname, server_port: this.inbound.listen_port })
this.inbound.addrs?.push(<Addr>{ server: location.hostname, server_port: this.inbound.listen_port })
},
closeModal() {
this.updateData() // reset
@@ -193,14 +205,23 @@ export default {
},
saveChanges() {
this.loading = true
this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value, this.inData)
this.$emit('save', this.inbound)
this.loading = false
},
},
computed: {
validate() {
if (this.inbound == undefined) return false
if (this.inbound.tag == "") return false
if (this.inbound.listen_port > 65535 || this.inbound.listen_port < 1) return false
if (this.OnlyTLS.includes(this.inbound.type) && this.inbound.tls_id == 0) return false
return true
},
},
watch: {
visible(newValue) {
if (newValue) {
this.updateData()
this.loading = true
}
},
},
+5 -9
View File
@@ -54,7 +54,6 @@
<Shadowsocks v-if="outbound.type == outTypes.Shadowsocks" direction="out" :data="outbound" />
<Vmess v-if="outbound.type == outTypes.VMess" :data="outbound" />
<Trojan v-if="outbound.type == outTypes.Trojan" :data="outbound" />
<Wireguard v-if="outbound.type == outTypes.Wireguard" :data="outbound" />
<Hysteria v-if="outbound.type == outTypes.Hysteria" direction="out" :data="outbound" />
<ShadowTls v-if="outbound.type == outTypes.ShadowTLS" :data="outbound" />
<Vless v-if="outbound.type == outTypes.VLESS" :data="outbound" />
@@ -69,7 +68,6 @@
<OutTLS v-if="Object.hasOwn(outbound,'tls')" :outbound="outbound" />
<Multiplex v-if="Object.hasOwn(outbound,'multiplex')" direction="out" :data="outbound" />
<Dial v-if="!NoDial.includes(outbound.type)" :dial="outbound" :outTags="tags" />
<v-switch v-model="outboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-window-item>
<v-window-item value="t2">
<v-row>
@@ -131,7 +129,7 @@ import Selector from '@/components/protocols/Selector.vue'
import UrlTest from '@/components/protocols/UrlTest.vue'
import HttpUtils from '@/plugins/httputil'
export default {
props: ['visible', 'data', 'id', 'stats', 'tags'],
props: ['visible', 'data', 'id', 'tags'],
emits: ['close', 'save'],
data() {
return {
@@ -141,14 +139,13 @@ export default {
link: "",
loading: false,
outTypes: OutTypes,
outboundStats: false,
NoDial: [OutTypes.Block, OutTypes.DNS, OutTypes.Selector, OutTypes.URLTest],
NoServer: [OutTypes.Direct, OutTypes.Block, OutTypes.DNS, OutTypes.Selector, OutTypes.URLTest, OutTypes.Tor],
}
},
methods: {
updateData() {
if (this.$props.id != -1) {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.outbound = createOutbound(newData.type, newData)
this.title = "edit"
@@ -158,13 +155,12 @@ export default {
this.title = "add"
}
this.tab = "t1"
this.outboundStats = this.$props.stats
},
changeType() {
// Tag change only in add outbound
const tag = this.$props.id != -1 ? this.outbound.tag : this.outbound.type + "-" + RandomUtil.randomSeq(3)
const tag = this.$props.id > 0 ? this.outbound.tag : this.outbound.type + "-" + RandomUtil.randomSeq(3)
// Use previous data
const prevConfig = { tag: tag ,listen: this.outbound.listen, listen_port: this.outbound.listen_port }
const prevConfig = { id: this.outbound.id, tag: tag ,listen: this.outbound.listen, listen_port: this.outbound.listen_port }
this.outbound = createOutbound(this.outbound.type, prevConfig)
},
closeModal() {
@@ -173,7 +169,7 @@ export default {
},
saveChanges() {
this.loading = true
this.$emit('save', this.outbound, this.outboundStats)
this.$emit('save', this.outbound)
this.loading = false
},
async linkConvert() {
+10 -8
View File
@@ -298,11 +298,11 @@ import { push } from 'notivue'
import { i18n } from '@/locales'
import RandomUtil from '@/plugins/randomUtil'
export default {
props: ['visible', 'data', 'index'],
props: ['visible', 'data', 'id'],
emits: ['close', 'save'],
data() {
return {
tls: { id: -1, name: '', inbounds: [], server: <iTls>{ enabled: true }, client: <oTls>{} },
tls: { id: 0, name: '', server: <iTls>{ enabled: true }, client: <oTls>{} },
title: "add",
loading: false,
menu: false,
@@ -354,15 +354,17 @@ export default {
},
methods: {
updateData() {
if (this.$props.index != -1) {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.tls = newData
if (this.tls.server == null) this.tls.server = {}
if (this.tls.client == null) this.tls.client = {}
this.tlsType = newData.server?.reality == undefined ? 0 : 1
this.usePath = newData.server?.key == undefined ? 0 : 1
this.title = "edit"
}
else {
this.tls = { id: 0, name: '', inbounds: [], server: {enabled: true}, client: {} }
this.tls = { id: 0, name: '', server: {enabled: true}, client: {} }
this.tlsType = 0
this.usePath = 0
this.title = "add"
@@ -461,10 +463,10 @@ export default {
},
computed: {
inTls(): iTls {
return <iTls> this.tls.server
return this.tls.server
},
outTls(): oTls {
return <oTls> this.tls.client
return this.tls.client
},
certText: {
get(): string { return this.inTls.certificate ? this.inTls.certificate.join('\n') : '' },
@@ -476,11 +478,11 @@ export default {
},
disableSni: {
get() { return this.outTls.disable_sni ?? false },
set(v: boolean) { this.outTls.disable_sni = v ? true : undefined }
set(v: boolean) { this.tls.client.disable_sni = v ? true : undefined }
},
insecure: {
get() { return this.outTls.insecure ?? false },
set(v: boolean) { this.outTls.insecure = v ? true : undefined }
set(v: boolean) { this.tls.client.insecure = v ? true : undefined }
},
server_port: {
get() { return this.inTls.reality?.handshake?.server_port ? this.inTls.reality.handshake.server_port : 443 },
+5 -2
View File
@@ -1,3 +1,5 @@
import { en } from "vuetify/lib/locale/index.mjs";
export default {
message: "Welcome",
success: "success",
@@ -37,6 +39,7 @@ export default {
home: "Home",
inbounds: "Inbounds",
outbounds: "Outbounds",
endpoints: "Endpoints",
clients: "Clients",
rules: "Rules",
tls: "TLS Settings",
@@ -75,6 +78,8 @@ export default {
inbound: "Inbound",
client: "Client",
outbound: "Outbound",
endpoint: "Endpoint",
config: "Config",
rule: "Rule",
user: "User",
tag: "Tag",
@@ -228,7 +233,6 @@ export default {
worker: "Workers",
ifName: "Interface Name",
sysIf: "System Interface",
gso: "Segmentation Offload",
options: "Wireguard Options",
multiPeer: "Multi Peer",
allowedIp: "Allowed IPs",
@@ -394,7 +398,6 @@ export default {
download: "Download",
volume: "Volume",
usage: "Usage",
enable: "Enable Statistics",
graphTitle: "Traffic Chart",
B: "B",
KB: "KB",
+5 -2
View File
@@ -1,3 +1,5 @@
import { config } from "process";
export default {
message: "خوش آمدید",
success: "موفق",
@@ -37,6 +39,7 @@ export default {
home: "خانه",
inbounds: "ورودی‌ها",
outbounds: "خروجی‌ها",
endpoints: "درگاه‌ها",
clients: "کاربران",
rules: "قوانین",
tls: "رمزنگاری‌ها",
@@ -75,6 +78,8 @@ export default {
inbound: "ورودی‌",
client: "کاربر",
outbound: "خروجی‌",
endpoint: "درگاه",
config: "پیکربندی",
rule: "قانون",
user: "کاربر",
tag: "برچسب",
@@ -227,7 +232,6 @@ export default {
worker: "عملگرها",
ifName: "نام اینترفیس",
sysIf: "استفاده از اینترفیس سیستم",
gso: "بارگذاری تقسیم‌بندی عمومی",
options: "گزینه‌های Wireguard",
multiPeer: "چند همتایی",
allowedIp: "آدرس‌های مجاز",
@@ -393,7 +397,6 @@ export default {
download: "دانلود",
volume: "حجم",
usage: "استفاده",
enable: "فعال سازی کنترل ترافیک",
graphTitle: "نمودار ترافیک",
B: "ب",
KB: "ک‌ب",
+5 -2
View File
@@ -1,3 +1,5 @@
import { config } from "process";
export default {
message: "Добро пожаловать",
success: "успех",
@@ -37,6 +39,7 @@ export default {
home: "Главная",
inbounds: "Входящие",
outbounds: "Исходящие",
endpoints: "Эндпоинты",
clients: "Клиенты",
rules: "Правила",
tls: "Настройки TLS",
@@ -75,6 +78,8 @@ export default {
inbound: "Входящий",
client: "Клиент",
outbound: "Исходящий",
endpoint: "Точка входа",
config: "Настройки",
rule: "Правило",
user: "Пользователь",
tag: "Тег",
@@ -228,7 +233,6 @@ export default {
worker: "Работники",
ifName: "Имя интерфейса",
sysIf: "Системный интерфейс",
gso: "Отключение сегментации",
options: "Параметры Wireguard",
multiPeer: "Множественный пир",
allowedIp: "Разрешенные IP",
@@ -394,7 +398,6 @@ export default {
download: "Скачивание",
volume: "Объем",
usage: "Использование",
enable: "Включить статистику",
graphTitle: "График трафика",
B: "Б",
KB: "КБ",
+3 -2
View File
@@ -37,6 +37,7 @@ export default {
home: "Trang chủ",
inbounds: "Đầu Vào",
outbounds: "Đầu ra",
endpoints: "Câu hình",
clients: "Khách hàng",
rules: "Quy tắc",
tls: "Cài đặt TLS",
@@ -75,6 +76,8 @@ export default {
inbound: "Đầu Vào",
client: "Máy Khách hàng",
outbound: "Đầu Ra",
endpoint: "Điểm cuối",
config: "Câu hình",
rule: "Quy tắc",
user: "Người dùng",
tag: "Thẻ",
@@ -228,7 +231,6 @@ export default {
worker: "Công nhân",
ifName: "Tên Giao diện",
sysIf: "Giao diện Hệ thống",
gso: "Giao Thức GSO",
options: "Tùy chọn Wireguard",
multiPeer: "Nhiều Đối tác",
allowedIp: "IPs được Phép",
@@ -395,7 +397,6 @@ export default {
download: "Tải xuống",
volume: "Thể tích",
usage: "Sử dụng",
enable: "Kích hoạt thống kê",
graphTitle: "Biểu đồ lưu lượng",
B: "B",
KB: "KB",
+3 -2
View File
@@ -37,6 +37,7 @@ export default {
home: "主页",
inbounds: "入站管理",
outbounds: "出站管理",
endpoints: "节点管理",
clients: "用户管理",
rules: "路由列表",
tls: "TLS 设置",
@@ -75,6 +76,8 @@ export default {
inbound: "入站",
client: "客户端",
outbound: "出站",
endpoint: "节点",
config: "配置",
rule: "规则",
user: "用户",
tag: "标签",
@@ -228,7 +231,6 @@ export default {
worker: "工作线程",
ifName: "接口名称",
sysIf: "系统接口",
gso: "分段卸载",
options: "WireGuard 选项",
multiPeer: "多对等体",
allowedIp: "允许的 IP 地址",
@@ -395,7 +397,6 @@ export default {
download: "下载",
volume: "流量",
usage: "已用",
enable: "启用统计",
graphTitle: "流量图表",
B: "B",
KB: "KB",
+3 -2
View File
@@ -38,6 +38,7 @@ export default {
home: "主頁",
inbounds: "入站管理",
outbounds: "出站管理",
endpoints: "端點管理",
clients: "用戶管理",
rules: "路由列表",
tls: "TLS 設置",
@@ -76,6 +77,8 @@ export default {
inbound: "入站",
client: "客戶端",
outbound: "出站",
endpoint: "端點",
config: "配置",
rule: "規則",
user: "用戶",
tag: "標簽",
@@ -229,7 +232,6 @@ export default {
worker: "工作線程",
ifName: "介面名稱",
sysIf: "系統介面",
gso: "分段卸載",
options: "Wireguard 選項",
multiPeer: "多對等方",
allowedIp: "允許的 IP",
@@ -396,7 +398,6 @@ export default {
download: "下載",
volume: "流量",
usage: "已用",
enable: "啟用統計",
graphTitle: "流量圖表",
B: "B",
KB: "KB",
-15
View File
@@ -1,15 +0,0 @@
export interface Addr {
server: string
server_port: number
tls?: boolean
insecure?: boolean
server_name?: string
remark?: string
}
export interface InData {
id: number
tag: string
addrs: Addr[]
outJson: any
}
+59 -59
View File
@@ -15,24 +15,24 @@ function utf8ToBase64(utf8String: string): string {
}
export namespace LinkUtil {
export function linkGenerator(user: Client, inbound: Inbound, tlsClient: any = {}, addrs: any[] = []): string[] {
export function linkGenerator(user: Client, inbound: Inbound, tls: any = {}, addrs: any[] = []): string[] {
switch(inbound.type){
case InTypes.Shadowsocks:
return shadowsocksLink(user,<Shadowsocks>inbound, addrs)
case InTypes.Naive:
return naiveLink(user,<Naive>inbound, addrs, tlsClient)
return naiveLink(user,<Naive>inbound, addrs, tls)
case InTypes.Hysteria:
return hysteriaLink(user,<Hysteria>inbound, addrs, tlsClient)
return hysteriaLink(user,<Hysteria>inbound, addrs, tls)
case InTypes.Hysteria2:
return hysteria2Link(user,<Hysteria2>inbound, addrs, tlsClient)
return hysteria2Link(user,<Hysteria2>inbound, addrs, tls)
case InTypes.TUIC:
return tuicLink(user,<TUIC>inbound, addrs, tlsClient)
return tuicLink(user,<TUIC>inbound, addrs, tls)
case InTypes.VLESS:
return vlessLink(user,<VLESS>inbound, addrs, tlsClient)
return vlessLink(user,<VLESS>inbound, addrs, tls)
case InTypes.Trojan:
return trojanLink(user,<Trojan>inbound, addrs, tlsClient)
return trojanLink(user,<Trojan>inbound, addrs, tls)
case InTypes.VMess:
return vmessLink(user,<VMess>inbound, addrs, tlsClient)
return vmessLink(user,<VMess>inbound, addrs, tls)
}
return []
}
@@ -71,17 +71,17 @@ export namespace LinkUtil {
return links
}
function hysteriaLink(user: Client, inbound: Hysteria, addrs: any[], tlsClient: any): string[] {
function hysteriaLink(user: Client, inbound: Hysteria, addrs: any[], tls: any): string[] {
const auth = user.config.hysteria.auth_str
const params = {
upmbps: inbound.up_mbps?? null,
downmbps: inbound.down_mbps?? null,
auth: auth?? null,
peer: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
peer: tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
obfsParam: inbound.obfs?? null,
fastopen: inbound.tcp_fast_open? 1 : 0,
insecure: tlsClient?.insecure ? 1 : null
insecure: tls?.client?.insecure ? 1 : null
}
let links = <string[]>[]
@@ -105,12 +105,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('peer', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('peer', inbound.tls.server_name) : uri.searchParams.delete('peer')
tls?.server?.server_name ? uri.searchParams.set('peer', tls?.server?.server_name) : uri.searchParams.delete('peer')
}
if (a.insecure) {
uri.searchParams.set('insecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
tls?.client?.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -119,17 +119,17 @@ export namespace LinkUtil {
return links
}
function hysteria2Link(user: Client, inbound: Hysteria2, addrs: any[], tlsClient: any): string[] {
function hysteria2Link(user: Client, inbound: Hysteria2, addrs: any[], tls: any): string[] {
const password = user.config.hysteria2.password
const params = {
upmbps: inbound.up_mbps?? null,
downmbps: inbound.down_mbps?? null,
sni: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
sni: tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
obfs: inbound.obfs?.type?? null,
'obfs-password': inbound.obfs?.password?? null,
fastopen: inbound.tcp_fast_open? 1 : 0,
insecure: tlsClient?.insecure ? 1 : null
insecure: tls?.client?.insecure ? 1 : null
}
let links = <string[]>[]
@@ -153,12 +153,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
tls?.server?.server_name ? uri.searchParams.set('sni', tls?.server?.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('insecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
tls?.client?.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -167,17 +167,17 @@ export namespace LinkUtil {
return links
}
function naiveLink(user: Client, inbound: Naive, addrs: any[], tlsClient: any): string[] {
function naiveLink(user: Client, inbound: Naive, addrs: any[], tls: any): string[] {
const password = user.config.naive.password
let links = <string[]>[]
if (addrs.length == 0) {
const params = {
padding: 1,
peer: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
peer: tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
tfo: inbound.tcp_fast_open? 1 : 0,
allowInsecure: tlsClient?.insecure ? 1 : null
allowInsecure: tls?.client?.insecure ? 1 : null
}
const uri = `http2://${utf8ToBase64(user.name + ":" + password + "@" + location.hostname + ":" + inbound.listen_port)}`
const paramsArray = []
@@ -191,10 +191,10 @@ export namespace LinkUtil {
addrs.forEach(a => {
const params = {
padding: 1,
peer: a.server_name?.length>0 ? a.server_name : inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
peer: a.server_name?.length>0 ? a.server_name : tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
tfo: inbound.tcp_fast_open? 1 : 0,
allowInsecure: a.insecure ? 1 : tlsClient?.insecure ? 1 : null
allowInsecure: a.insecure ? 1 : tls?.client?.insecure ? 1 : null
}
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + a.server + ":" + a.server_port)}`
const paramsArray = []
@@ -209,14 +209,14 @@ export namespace LinkUtil {
return links
}
function tuicLink(user: Client, inbound: TUIC, addrs: any[], tlsClient: any): string[] {
function tuicLink(user: Client, inbound: TUIC, addrs: any[], tls: any): string[] {
const u = user.config.tuic
const params = {
sni: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
sni: tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
congestion_control: inbound.congestion_control?? null,
allowInsecure: tlsClient?.insecure ? 1 : null,
disable_sni: tlsClient?.disable_sni ? 1 : null
allowInsecure: tls?.client?.insecure ? 1 : null,
disable_sni: tls?.client?.disable_sni ? 1 : null
}
let links = <string[]>[]
@@ -240,12 +240,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
tls?.server?.server_name ? uri.searchParams.set('sni', tls?.server?.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
tls?.client?.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -287,7 +287,7 @@ export namespace LinkUtil {
return params
}
function vlessLink(user: Client, inbound: VLESS, addrs: any[], tlsClient: any): string[] {
function vlessLink(user: Client, inbound: VLESS, addrs: any[], tls: any): string[] {
const u = user.config.vless
const transport = <Transport>inbound.transport
@@ -295,14 +295,14 @@ export namespace LinkUtil {
const params = {
type: transport?.type?? 'tcp',
security: inbound.tls?.enabled? inbound.tls?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: inbound.tls?.alpn?.join(',')?? null,
sni: inbound.tls?.server_name?? null,
flow: inbound.tls?.enabled ? u?.flow?? null : null,
allowInsecure: tlsClient?.insecure ? 1 : null,
fp: tlsClient?.utls?.enabled ? tlsClient.utls.fingerprint : null,
pbk: tlsClient?.reality?.public_key?? null,
sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[RandomUtil.randomInt(inbound.tls.reality.short_id.length)] : null) : null
security: tls?.server?.enabled? tls?.server?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: tls?.server?.alpn?.join(',')?? null,
sni: tls?.server?.server_name?? null,
flow: tls?.server?.enabled ? u?.flow?? null : null,
allowInsecure: tls?.client?.insecure ? 1 : null,
fp: tls?.client?.utls?.enabled ? tls.client.utls.fingerprint : null,
pbk: tls?.client?.reality?.public_key?? null,
sid: tls?.server?.reality?.enabled ? (tls?.server?.reality?.short_id?.length>0 ? tls.server.reality.short_id[RandomUtil.randomInt(tls.server.reality.short_id.length)] : null) : null
}
let links = <string[]>[]
if (addrs.length == 0) {
@@ -335,12 +335,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls?.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
tls?.server?.server_name ? uri.searchParams.set('sni', tls?.server?.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
tls?.client?.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -349,7 +349,7 @@ export namespace LinkUtil {
return links
}
function trojanLink(user: Client, inbound: Trojan, addrs: any[], tlsClient: any): string[] {
function trojanLink(user: Client, inbound: Trojan, addrs: any[], tls: any): string[] {
const u = user.config.trojan
const transport = <Transport>inbound.transport
@@ -357,13 +357,13 @@ export namespace LinkUtil {
const params = {
type: transport?.type?? 'tcp',
security: inbound.tls?.enabled? inbound.tls?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: inbound.tls?.alpn?.join(',')?? null,
sni: inbound.tls?.server_name?? null,
allowInsecure: tlsClient?.insecure ? 1 : null,
fp: tlsClient?.utls?.enabled ? tlsClient.utls.fingerprint : null,
pbk: tlsClient?.reality?.public_key?? null,
sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[RandomUtil.randomInt(inbound.tls.reality.short_id.length)] : null) : null
security: tls?.server?.enabled? tls?.server?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: tls?.server?.alpn?.join(',')?? null,
sni: tls?.server?.server_name?? null,
allowInsecure: tls?.client?.insecure ? 1 : null,
fp: tls?.client?.utls?.enabled ? tls.client.utls.fingerprint : null,
pbk: tls?.client?.reality?.public_key?? null,
sid: tls?.server?.reality?.enabled ? (tls?.server?.reality?.short_id?.length>0 ? tls?.server?.reality.short_id[RandomUtil.randomInt(tls?.server?.reality.short_id.length)] : null) : null
}
let links = <string[]>[]
@@ -397,12 +397,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls?.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
tls?.server?.server_name ? uri.searchParams.set('sni', tls?.server?.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
tls?.client?.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -411,7 +411,7 @@ export namespace LinkUtil {
return links
}
function vmessLink(user: Client, inbound: VMess, addrs: any[], tlsClient: any): string[] {
function vmessLink(user: Client, inbound: VMess, addrs: any[], tls: any): string[] {
const u = user.config.vmess
const transport = <Transport>inbound.transport
@@ -429,9 +429,9 @@ export namespace LinkUtil {
path: tParams.path?? undefined,
port: inbound.listen_port,
ps: inbound.tag,
sni: inbound.tls.server_name?? undefined,
tls: Object.keys(inbound.tls).length>0? 'tls' : 'none',
allowInsecure: tlsClient?.insecure ? 1 : undefined
sni: tls?.server?.server_name?? undefined,
tls: tls?.server && Object.keys(tls.server).length>0? 'tls' : 'none',
allowInsecure: tls?.client?.insecure ? 1 : undefined
}
let links = <string[]>[]
if (addrs.length == 0) {
+18 -19
View File
@@ -3,50 +3,49 @@ import { iTls } from "@/types/inTls"
import { oTls } from "@/types/outTls"
import RandomUtil from "./randomUtil"
export function fillData(out: any, inbound: Inbound, tlsClient: any) {
if (Object.hasOwn(inbound, 'tls')) {
const inb = <any>inbound
addTls(out,inb.tls,tlsClient)
export function fillData(inbound: Inbound, tls: any | null = null) {
if (tls != null) {
addTls(inbound.out_json, tls.server, tls.client)
} else {
delete out.tls
delete inbound.out_json.tls
}
out.type = inbound.type
out.tag = inbound.tag
out.server = location.hostname
out.server_port = inbound.listen_port
inbound.out_json.type = inbound.type
inbound.out_json.tag = inbound.tag
inbound.out_json.server = location.hostname
inbound.out_json.server_port = inbound.listen_port
switch(inbound.type){
case InTypes.HTTP: case InTypes.SOCKS: case InTypes.Mixed:
return
case InTypes.Shadowsocks:
shadowsocksOut(out, <Shadowsocks>inbound)
shadowsocksOut(inbound.out_json, <Shadowsocks>inbound)
return
case InTypes.ShadowTLS:
shadowTlsOut(out, <ShadowTLS>inbound)
shadowTlsOut(inbound.out_json, <ShadowTLS>inbound)
return
case InTypes.Hysteria:
hysteriaOut(out, <Hysteria>inbound)
hysteriaOut(inbound.out_json, <Hysteria>inbound)
return
case InTypes.Hysteria2:
hysteria2Out(out, <Hysteria2>inbound)
hysteria2Out(inbound.out_json, <Hysteria2>inbound)
return
case InTypes.TUIC:
tuicOut(out, <TUIC>inbound)
tuicOut(inbound.out_json, <TUIC>inbound)
return
case InTypes.VLESS:
vlessOut(out, <VLESS>inbound)
vlessOut(inbound.out_json, <VLESS>inbound)
return
case InTypes.Trojan:
trojanOut(out, <Trojan>inbound)
trojanOut(inbound.out_json, <Trojan>inbound)
return
case InTypes.VMess:
vmessOut(out, <VMess>inbound)
vmessOut(inbound.out_json, <VMess>inbound)
return
}
Object.keys(out).forEach(key => delete out[key])
Object.keys(inbound.out_json).forEach(key => delete inbound.out_json[key])
}
function addTls(out: any, tls: iTls, tlsClient: oTls){
out.tls = tlsClient
out.tls = tlsClient?? {}
if(tls.enabled) out.tls.enabled = tls.enabled
if(tls.server_name) out.tls.server_name = tls.server_name
if(tls.alpn) out.tls.alpn = tls.alpn
+188
View File
@@ -0,0 +1,188 @@
const WgUtil = {
gf(init?: Float64Array): Float64Array {
var r = new Float64Array(16)
if (init) {
for (var i = 0; i < init.length; ++i)
r[i] = init[i]
}
return r
},
pack(o: Float64Array, n: Float64Array): Float64Array {
var b, m = this.gf(), t = this.gf()
for (var i = 0; i < 16; ++i)
t[i] = n[i]
this.carry(t)
this.carry(t)
this.carry(t)
for (var j = 0; j < 2; ++j) {
m[0] = t[0] - 0xffed
for (var i = 1; i < 15; ++i) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1)
m[i - 1] &= 0xffff
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1)
b = (m[15] >> 16) & 1
m[14] &= 0xffff
this.cswap(t, m, 1 - b)
}
for (var i = 0; i < 16; ++i) {
o[2 * i] = t[i] & 0xff
o[2 * i + 1] = t[i] >> 8
}
},
carry(o: Float64Array) {
var c
for (var i = 0; i < 16; ++i) {
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536)
o[i] &= 0xffff
}
},
cswap(p: Float64Array, q: Float64Array, b: number) {
var t, c = ~(b - 1)
for (var i = 0; i < 16; ++i) {
t = c & (p[i] ^ q[i])
p[i] ^= t
q[i] ^= t
}
},
add(o: Float64Array, a: Float64Array, b: Float64Array) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] + b[i]) | 0
},
subtract(o: Float64Array, a: Float64Array, b: Float64Array) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] - b[i]) | 0
},
multmod(o: Float64Array, a: Float64Array, b: Float64Array) {
var t = new Float64Array(31)
for (var i = 0; i < 16; ++i) {
for (var j = 0; j < 16; ++j)
t[i + j] += a[i] * b[j]
}
for (var i = 0; i < 15; ++i)
t[i] += 38 * t[i + 16]
for (var i = 0; i < 16; ++i)
o[i] = t[i]
this.carry(o)
this.carry(o)
},
invert(o: Float64Array, i: Float64Array) {
var c = this.gf()
for (var a = 0; a < 16; ++a)
c[a] = i[a]
for (var a = 253; a >= 0; --a) {
this.multmod(c, c, c)
if (a !== 2 && a !== 4)
this.multmod(c, c, i)
}
for (var a = 0; a < 16; ++a)
o[a] = c[a]
},
clamp(z) {
z[31] = (z[31] & 127) | 64
z[0] &= 248
},
generatePublicKey(privateKey: Uint8Array): Uint8Array {
var r, z = new Uint8Array(32)
var a = this.gf([1]),
b = this.gf([9]),
c = this.gf(),
d = this.gf([1]),
e = this.gf(),
f = this.gf(),
_121665 = this.gf([0xdb41, 1]),
_9 = this.gf([9])
for (var i = 0; i < 32; ++i)
z[i] = privateKey[i]
this.clamp(z)
for (var i = 254; i >= 0; --i) {
r = (z[i >>> 3] >>> (i & 7)) & 1
this.cswap(a, b, r)
this.cswap(c, d, r)
this.add(e, a, c)
this.subtract(a, a, c)
this.add(c, b, d)
this.subtract(b, b, d)
this.multmod(d, e, e)
this.multmod(f, a, a)
this.multmod(a, c, a)
this.multmod(c, b, e)
this.add(e, a, c)
this.subtract(a, a, c)
this.multmod(b, a, a)
this.subtract(c, d, f)
this.multmod(a, c, _121665)
this.add(a, a, d)
this.multmod(c, c, a)
this.multmod(a, d, f)
this.multmod(d, b, _9)
this.multmod(b, e, e)
this.cswap(a, b, r)
this.cswap(c, d, r)
}
this.invert(c, c)
this.multmod(a, a, c)
this.pack(z, a)
return z
},
generatePresharedKey(): Uint8Array {
var privateKey = new Uint8Array(32)
window.crypto.getRandomValues(privateKey)
return privateKey
},
generatePrivateKey(): Uint8Array {
var privateKey = this.generatePresharedKey()
this.clamp(privateKey)
return privateKey
},
encodeBase64(dest: Uint8Array, src: Uint8Array) {
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63])
for (var i = 0; i < 4; ++i)
dest[i] = input[i] + 65 +
(((25 - input[i]) >> 8) & 6) -
(((51 - input[i]) >> 8) & 75) -
(((61 - input[i]) >> 8) & 15) +
(((62 - input[i]) >> 8) & 3)
},
keyToBase64(key: Uint8Array): string {
var i, base64 = new Uint8Array(44)
for (i = 0; i < 32 / 3; ++i)
this.encodeBase64(base64.subarray(i * 4), key.subarray(i * 3))
this.encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]))
base64[43] = 61
return String.fromCharCode.apply(null, base64)
},
keyFromBase64(encoded: string): Uint8Array {
const binaryStr = atob(encoded)
const bytes = new Uint8Array(binaryStr.length)
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i)
}
return bytes
},
generateKeypair(secretKey?: string ='') {
var privateKey = secretKey.length>0 ? this.keyFromBase64(secretKey) : this.generatePrivateKey()
var publicKey = this.generatePublicKey(privateKey)
return {
publicKey: this.keyToBase64(publicKey),
privateKey: secretKey.length>0 ? secretKey : this.keyToBase64(privateKey)
}
}
}
export default WgUtil
+5
View File
@@ -34,6 +34,11 @@ const routes = [
name: 'pages.outbounds',
component: () => import('@/views/Outbounds.vue'),
},
{
path: '/endpoints',
name: 'pages.endpoints',
component: () => import('@/views/Endpoints.vue'),
},
{
path: '/rules',
name: 'pages.rules',
+41 -78
View File
@@ -3,6 +3,9 @@ import HttpUtils from '@/plugins/httputil'
import { defineStore } from 'pinia'
import { push } from 'notivue'
import { i18n } from '@/locales'
import { Inbound } from '@/types/inbounds'
import { Outbound } from '@/types/outbounds'
import { Endpoint } from '@/types/endpoints'
const Data = defineStore('Data', {
state: () => ({
@@ -10,23 +13,17 @@ const Data = defineStore('Data', {
reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[],
subURI: "",
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
oldData: <{config: any, clients: any[], tlsConfigs: any[], inData: any[]}>{},
config: <any>{},
inbounds: <Inbound[]>[],
outbounds: <Outbound[]>[],
endpoints: <Endpoint[]>[],
clients: [],
tlsConfigs: [],
inData: [],
tlsConfigs: <any[]>[],
}),
actions: {
async loadData() {
const msg = await HttpUtils.get('api/load', this.lastLoad >0 ? {lu: this.lastLoad} : {} )
if(msg.success) {
this.lastLoad = Math.floor((new Date()).getTime()/1000)
// Set new data
if (msg.obj.config) this.oldData.config = msg.obj.config
if (msg.obj.clients) this.oldData.clients = msg.obj.clients
if (msg.obj.tls) this.oldData.tlsConfigs = msg.obj.tls
if (msg.obj.inData) this.oldData.inData = msg.obj.inData
this.onlines = msg.obj.onlines
if (msg.obj.lastLog) {
push.error({
@@ -37,84 +34,50 @@ const Data = defineStore('Data', {
}
if (msg.obj.config) {
// To avoid ref copy
const data = JSON.parse(JSON.stringify(msg.obj))
if (data.subURI) this.subURI = data.subURI
if (data.config) this.config = data.config
if (data.clients) this.clients = data.clients
if (data.tls) this.tlsConfigs = data.tls
if (data.inData) this.inData = data.inData
this.setNewData(msg.obj)
}
}
},
async pushData() {
const diff = {
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config), null, 2),
clients: JSON.stringify(FindDiff.ArrObj(this.clients,this.oldData.clients, "clients"), null, 2),
tls: JSON.stringify(FindDiff.ArrObj(this.tlsConfigs,this.oldData.tlsConfigs, "tls"), null, 2),
inData: JSON.stringify(FindDiff.ArrObj(this.inData,this.oldData.inData, "inData"), null, 2),
}
const msg = await HttpUtils.post('api/save',diff)
if(msg.success) {
this.lastLoad = 0
this.loadData()
}
setNewData(data: any) {
this.lastLoad = Math.floor((new Date()).getTime()/1000)
if (data.subURI) this.subURI = data.subURI
if (data.config) this.config = data.config
if (data.clients) this.clients = data.clients
if (data.inbounds) this.inbounds = data.inbounds
if (data.outbounds) this.outbounds = data.outbounds
if (data.endpoints) this.endpoints = data.endpoints
if (data.tls) this.tlsConfigs = data.tls
},
async delInbound(index: number) {
const diff = {
config: JSON.stringify([{key: "inbounds", action: "del", index: index, obj: null}]),
clients: JSON.stringify(FindDiff.ArrObj(this.clients,this.oldData.clients, "clients"), null, 2),
tls: JSON.stringify(FindDiff.ArrObj(this.tlsConfigs,this.oldData.tlsConfigs, "tls"), null, 2),
inData: <string|undefined> undefined,
}
// Validate inData
let invalidInData = <any[]>[]
this.inData.forEach((d:any) => {
const inboundIndex = this.config.inbounds.findIndex((i:any) => i.tag == d.tag)
if (inboundIndex == -1) invalidInData.push({key: "inData", action: "del", index: d.id, obj: null})
})
if (invalidInData.length>0) {
diff.inData = JSON.stringify(invalidInData)
}
const msg = await HttpUtils.post('api/save',diff)
async loadInbounds(ids: number[]): Promise<Inbound[]> {
const options = ids.length > 0 ? {id: ids.join(",")} : {}
const msg = await HttpUtils.get('api/inbounds', options)
if(msg.success) {
this.loadData()
return msg.obj.inbounds
}
return <Inbound[]>[]
},
async delInData(id: number) {
const diff = {
inData: JSON.stringify([{key: "inData", action: "del", index: id, obj: null}])
async save (object: string, action: string, data: any, userLinks: any[] | null = null, outJsons: any[] | null = null): Promise<boolean> {
let postData = {
object: object,
action: action,
data: JSON.stringify(data),
userLinks: userLinks == null ? undefined : JSON.stringify(userLinks),
outJsons: outJsons == null ? undefined : JSON.stringify(outJsons),
}
await HttpUtils.post('api/save',diff)
},
async delOutbound(index: number) {
const diff = {
config: JSON.stringify([{key: "outbounds", action: "del", index: index, obj: null}]),
if (userLinks == null) {
delete postData.userLinks
}
const msg = await HttpUtils.post('api/save',diff)
if(msg.success) {
this.loadData()
}
},
async delClient(id: number) {
const diff = {
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config)),
clients:JSON.stringify([{key: "clients", action: "del", index: id, obj: null}]),
}
const msg = await HttpUtils.post('api/save',diff)
if(msg.success) {
this.loadData()
}
},
async delTls(id: number) {
const diff = {
tls:JSON.stringify([{key: "tls", action: "del", index: id, obj: null}]),
}
const msg = await HttpUtils.post('api/save',diff)
if(msg.success) {
this.loadData()
const msg = await HttpUtils.post('api/save', postData)
if (msg.success) {
const objectName = ['tls', 'config'].includes(object) ? object : object.substring(0, object.length - 1)
push.success({
title: i18n.global.t('success'),
duration: 5000,
message: i18n.global.t('actions.' + action) + " " + i18n.global.t('objects.' + objectName)
})
this.setNewData(msg.obj)
}
return msg.success
}
},
})
+1 -1
View File
@@ -6,7 +6,7 @@ export interface Client {
enable: boolean
name: string
config: Config
inbounds: string[]
inbounds: number[]
links: Link[]
volume: number
expiry: number
-23
View File
@@ -83,8 +83,6 @@ interface RouteRuleSet {
interface Experimental {
cache_file?: CacheFile
clash_api?: ClashApi
v2ray_api: V2rayApi
}
interface CacheFile {
@@ -94,27 +92,6 @@ interface CacheFile {
store_fakeip?: boolean
}
interface V2rayApi {
listen: string
stats: V2rayApiStats
}
export interface V2rayApiStats {
enabled: boolean
inbounds: string[]
outbounds: string[]
users: string[]
}
interface ClashApi {
external_controller?: string
external_ui?: string
external_ui_download_url?: string
external_ui_download_detour?: string
secret?: string
default_mode?: string
}
export interface Config {
log: Log
dns: Dns
+58
View File
@@ -0,0 +1,58 @@
import { Dial } from "./outbounds"
export const EpTypes = {
Wireguard: 'wireguard',
}
type EpType = typeof EpTypes[keyof typeof EpTypes]
interface EndpointBasics {
id: number
type: EpType
tag: string
}
export interface WgPeer {
address: string
port: number
public_key: string
pre_shared_key?: string
allowed_ips?: string[]
persistent_keepalive_interval?: number
reserved?: number[]
}
export interface WireGuard extends EndpointBasics, Dial {
system?: boolean
name?: string
mtu?: number
address: string[]
private_key: string
listen_port: number,
peers: WgPeer[]
udp_timeout?: string,
workers?: number
}
// Create interfaces dynamically based on EpTypes keys
type InterfaceMap = {
[Key in keyof typeof EpTypes]: {
type: string
[otherProperties: string]: any; // You can add other properties as needed
}
}
// Create union type from InterfaceMap
export type Endpoint = InterfaceMap[keyof InterfaceMap]
// Create defaultValues object dynamically
const defaultValues: Record<EpType, Endpoint> = {
wireguard: { type: EpTypes.Wireguard, address: ['10.0.0.2/32','fe80::2/128'], private_key: '', listen_port: 0, peers: [{ address: '', port: 0, public_key: ''}] },
}
export function createEndpoint<T extends Endpoint>(type: string,json?: Partial<T>): Endpoint {
const defaultObject: Endpoint = { ...defaultValues[type], ...(json || {}) }
return defaultObject
}
+24 -14
View File
@@ -24,6 +24,15 @@ export const InTypes = {
type InType = typeof InTypes[keyof typeof InTypes]
export interface Addr {
server: string
server_port: number
tls?: boolean
insecure?: boolean
server_name?: string
remark?: string
}
export interface Listen {
listen: string
listen_port: number
@@ -39,8 +48,12 @@ export interface Listen {
}
interface InboundBasics extends Listen {
id: number
type: InType
tag: string
tls_id: number
addrs?: Addr[]
out_json?: any
}
interface UsernamePass {
@@ -87,7 +100,6 @@ export interface SOCKS extends InboundBasics {
}
export interface HTTP extends InboundBasics {
users?: UsernamePass[]
tls?: iTls,
}
export interface Shadowsocks extends InboundBasics {
method: string
@@ -125,7 +137,6 @@ export interface Hysteria extends InboundBasics {
recv_window_client?: number
max_conn_client?: number
disable_mtu_discovery?: boolean
tls: iTls
}
export interface ShadowTLS extends InboundBasics {
version: 1|2|3
@@ -139,7 +150,6 @@ export interface ShadowTLS extends InboundBasics {
}
export interface VLESS extends InboundBasics {
users: VlessUser[]
tls?: iTls
multiplex?: iMultiplex
transport?: Transport
}
@@ -149,7 +159,6 @@ export interface TUIC extends InboundBasics {
auth_timeout?: string
zero_rtt_handshake?: boolean
heartbeat?: string
tls: iTls
}
export interface Hysteria2 extends InboundBasics {
up_mbps?: number
@@ -160,7 +169,6 @@ export interface Hysteria2 extends InboundBasics {
}
users: NamePass[]
ignore_client_bandwidth?: boolean
tls: iTls
masquerade?: string
brutal_debug?: boolean
}
@@ -234,24 +242,26 @@ type userEnabledTypes = {
vless: VLESS
}
export const inboundWithUsers = ['mixed', 'socks:', 'http', 'shadowsocks', 'vmess', 'trojan', 'naive', 'hysteria', 'shadowtls', 'tuic', 'hysteria2', 'vless']
// Create union type from userEnabledTypes
export type InboundWithUser = userEnabledTypes[keyof userEnabledTypes]
type InboundWithUser = userEnabledTypes[keyof userEnabledTypes]
// Create defaultValues object dynamically
const defaultValues: Record<InType, Inbound> = {
direct: <Direct>{ type: InTypes.Direct },
mixed: <Mixed>{ type: InTypes.Mixed },
socks: <SOCKS>{ type: InTypes.SOCKS },
http: <HTTP>{ type: InTypes.HTTP, tls: {} },
http: <HTTP>{ type: InTypes.HTTP, tls_id: 0 },
shadowsocks: <Shadowsocks>{ type: InTypes.Shadowsocks, method: 'none', multiplex: {} },
vmess: <VMess>{ type: InTypes.VMess, users: <VmessUser[]>[], tls: {}, multiplex: {}, transport: {} },
trojan: <Trojan>{ type: InTypes.Trojan, users: <NamePass[]>[], tls: {}, multiplex: {}, transport: {} },
naive: <Naive>{ type: InTypes.Naive, users: <UsernamePass[]>[], tls: { enabled: true } },
hysteria: <Hysteria>{ type: InTypes.Hysteria, users: <NameAuth[]>[], up_mbps: 100, down_mbps: 100, tls: { enabled: true } },
vmess: <VMess>{ type: InTypes.VMess, users: <VmessUser[]>[], tls_id: 0, multiplex: {}, transport: {} },
trojan: <Trojan>{ type: InTypes.Trojan, users: <NamePass[]>[], tls_id: 0, multiplex: {}, transport: {} },
naive: <Naive>{ type: InTypes.Naive, users: <UsernamePass[]>[], tls_id: 0 },
hysteria: <Hysteria>{ type: InTypes.Hysteria, users: <NameAuth[]>[], up_mbps: 100, down_mbps: 100, tls_id: 0 },
shadowtls: <ShadowTLS>{ type: InTypes.ShadowTLS, version: 3, users: <NamePass[]>[], handshake: {}, handshake_for_server_name: {} },
tuic: <TUIC>{ type: InTypes.TUIC, users: <TuicUser[]>[], congestion_control: "cubic", tls: { enabled: true } },
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls: { enabled: true } },
vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls: {}, multiplex: {}, transport: {} },
tuic: <TUIC>{ type: InTypes.TUIC, users: <TuicUser[]>[], congestion_control: "cubic", tls_id: 0 },
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls_id: 0 },
vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls_id: 0, multiplex: {}, transport: {} },
tun: <Tun>{ type: InTypes.Tun, mtu: 9000, stack: 'system', udp_timeout: '5m', auto_route: false },
redirect: <Redirect>{ type: InTypes.Redirect },
tproxy: <TProxy>{ type: InTypes.TProxy },
+1 -19
View File
@@ -10,7 +10,6 @@ export const OutTypes = {
Shadowsocks: 'shadowsocks',
VMess: 'vmess',
Trojan: 'trojan',
Wireguard: 'wireguard',
Hysteria: 'hysteria',
VLESS: 'vless',
ShadowTLS: 'shadowtls',
@@ -41,6 +40,7 @@ export interface Dial {
}
interface OutboundBasics {
id: number
type: OutType
tag: string
}
@@ -124,23 +124,6 @@ export interface Trojan extends OutboundBasics, Dial {
transport?: Transport
}
export interface WireGuard extends OutboundBasics, Dial {
server?: string
server_port?: number
system_interface?: boolean
gso?: boolean
interface_name?: string
local_address: string[]
private_key: string
peers?: WgPeer[]
peer_public_key?: string
pre_shared_key?: string
reserved?: number[]
workers?: number
mtu?: number
network?: "udp" | "tcp"
}
export interface Hysteria extends OutboundBasics, Dial {
server: string
server_port: number
@@ -263,7 +246,6 @@ const defaultValues: Record<OutType, Outbound> = {
shadowsocks: { type: OutTypes.Shadowsocks, method: 'none', multiplex: {} },
vmess: { type: OutTypes.VMess, tls: {}, multiplex: {}, transport: {}, security: 'auto', global_padding: false },
trojan: { type: OutTypes.Trojan, tls: {}, multiplex: {}, transport: {} },
wireguard: { type: OutTypes.Wireguard, local_address: ['10.0.0.2/32','fe80::2/128'], private_key: '' },
hysteria: { type: OutTypes.Hysteria, up_mbps: 100, down_mbps: 100, tls: { enabled: true } },
shadowtls: { type: OutTypes.ShadowTLS, version: 3, tls: { enabled: true } },
vless: { type: OutTypes.VLESS, tls: {}, multiplex: {}, transport: {} },
+35 -109
View File
@@ -1,4 +1,11 @@
<template>
<v-row style="margin-bottom: 10px;">
<v-col cols="12" justify="center" align="center">
<v-btn variant="outlined" color="warning" @click="saveConfig" :loading="loading" :disabled="stateChange">
{{ $t('actions.save') }}
</v-btn>
</v-col>
</v-row>
<v-expansion-panels>
<v-expansion-panel :title="$t('basic.log.title')">
<v-expansion-panel-text>
@@ -11,6 +18,8 @@
hide-details
:label="$t('basic.log.level')"
:items="levels"
clearable
@click:clear="delete appConfig.log.level"
v-model="appConfig.log.level">
</v-select>
</v-col>
@@ -215,128 +224,49 @@
hide-details></v-switch>
</v-col>
</v-row>
Clash API
<v-divider></v-divider>
<v-row>
<v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="enableClashApi" color="primary" :label="$t('enable')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.external_controller"
hide-details
label="External Controller"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.external_ui"
hide-details
label="External UI"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.external_ui_download_url"
hide-details
label="UI Download URL"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.external_ui_download_detour"
hide-details
label="UI Download detour"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.secret"
hide-details
label="Secret"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.default_mode"
hide-details
label="Default Mode"
></v-text-field>
</v-col>
</v-row>
V2Ray API
<v-divider></v-divider>
<v-row>
<v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
v-model="appConfig.experimental.v2ray_api.listen"
hide-details
:label="$t('objects.listen')"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="appConfig.experimental.v2ray_api.stats.enabled"
color="primary"
:label="$t('stats.enable')"
hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="appConfig.experimental.v2ray_api.stats.enabled">
<v-col cols="12" sm="6">
<v-select
hide-details
:label="$t('pages.inbounds')"
multiple chips closable-chips
:items="inboundTags"
v-model="appConfig.experimental.v2ray_api.stats.inbounds">
</v-select>
</v-col>
<v-col cols="12" sm="6">
<v-select
hide-details
:label="$t('pages.outbounds')"
multiple chips closable-chips
:items="outboundTags"
v-model="appConfig.experimental.v2ray_api.stats.outbounds">
</v-select>
</v-col>
<v-col cols="12" sm="6">
<v-select
hide-details
:label="$t('pages.clients')"
multiple chips closable-chips
:items="clientNames"
v-model="appConfig.experimental.v2ray_api.stats.users">
</v-select>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</template>
<script lang="ts" setup>
import Data from '@/store/modules/data';
import Dial from '@/components/Dial.vue';
import { computed } from 'vue';
import { Config, Ntp } from '@/types/config';
import { Client } from '@/types/clients';
import Data from '@/store/modules/data'
import Dial from '@/components/Dial.vue'
import { computed, ref, onMounted } from 'vue'
import { Config, Ntp } from '@/types/config'
import { Client } from '@/types/clients'
import { FindDiff } from '@/plugins/utils'
const oldConfig = ref({})
const loading = ref(false)
const appConfig = computed((): Config => {
return <Config> Data().config
})
const inboundTags = computed((): string[] => {
return appConfig.value.inbounds.map(i => i.tag)
onMounted(async () => {
oldConfig.value = JSON.parse(JSON.stringify(Data().config))
})
const stateChange = computed(() => {
return FindDiff.deepCompare(appConfig.value,oldConfig.value)
})
const saveConfig = async () => {
loading.value = true
const success = await Data().save("config", "set", appConfig.value)
if (success) {
oldConfig.value = JSON.parse(JSON.stringify(Data().config))
loading.value = false
}
}
const outboundTags = computed((): string[] => {
return appConfig.value.outbounds.map(o => o.tag)
return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)]
})
const clientNames = computed((): string[] => {
const clients = <Client[]>Data().clients
return clients?.map(c => c.name)
return Data().clients.map((c:any) => c.name)
})
const levels = ["trace", "debug", "info", "warn", "error", "fatal", "panic"]
@@ -384,8 +314,4 @@ const enableCacheFile = computed({
}
})
const enableClashApi = computed({
get() { return appConfig.value.experimental.clash_api != undefined },
set(v:boolean) { v ? appConfig.value.experimental.clash_api = {} : delete appConfig.value.experimental.clash_api }
})
</script>
+30 -112
View File
@@ -3,10 +3,9 @@
<ClientModal
v-model="modal.visible"
:visible="modal.visible"
:index="modal.index"
:id="modal.id"
:data="modal.data"
:groups="groups"
:stats="modal.stats"
:inboundTags="inboundTags"
@close="closeModal"
@save="saveModal"
@@ -34,7 +33,7 @@
/>
<v-row justify="center" align="center">
<v-col cols="auto">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
<v-col cols="auto">
<v-menu v-model="actionMenu" :close-on-content-click="false" location="bottom center">
@@ -147,7 +146,6 @@
<v-col cols="auto">
<v-switch color="primary"
v-model="item.enable"
@update:model-value="buildInboundsUsers(item.inbounds)"
hideDetails density="compact" />
</v-col>
</v-row>
@@ -162,7 +160,7 @@
<v-col>{{ $t('pages.inbounds') }}</v-col>
<v-col dir="ltr">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds != ''">
<span v-for="i in item.inbounds">{{ i }}<br /></span>
<span v-for="i in item.inbounds">{{ inbounds.find(inb => inb.id == i)?.tag }}<br /></span>
</v-tooltip>
{{ item.inbounds.length }}
</v-col>
@@ -230,7 +228,7 @@
<v-icon />
<v-tooltip activator="parent" location="top" text="QR-Code"></v-tooltip>
</v-btn>
<v-btn icon="mdi-chart-line" @click="showStats(item.name)" v-if="v2rayStats.users.includes(item.name)">
<v-btn icon="mdi-chart-line" @click="showStats(item.name)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
@@ -324,7 +322,7 @@
>
mdi-qrcode
</v-icon>
<v-icon icon="mdi-chart-line" @click="showStats(item.name)" v-if="v2rayStats.users.includes(item.name)">
<v-icon icon="mdi-chart-line" @click="showStats(item.name)">
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-icon>
</template>
@@ -349,8 +347,7 @@ import QrCode from '@/layouts/modals/QrCode.vue'
import Stats from '@/layouts/modals/Stats.vue'
import { Client, createClient } from '@/types/clients'
import { computed, ref } from 'vue'
import { Config, V2rayApiStats } from '@/types/config'
import { InTypes, Inbound,InboundWithUser, ShadowTLS, VLESS } from '@/types/inbounds'
import { Inbound, inboundWithUsers } from '@/types/inbounds'
import { Link, LinkUtil } from '@/plugins/link'
import { HumanReadable } from '@/plugins/utils'
import { i18n } from '@/locales'
@@ -367,21 +364,13 @@ const isOnline = (cname: string) => computed(() => {
return Data().onlines?.user ? Data().onlines.user.includes(cname) : false
})
const appConfig = computed((): Config => {
return <Config> Data().config
})
const v2rayStats = computed((): V2rayApiStats => {
return <V2rayApiStats> appConfig.value.experimental.v2ray_api.stats
})
const inbounds = computed((): Inbound[] => {
return <Inbound[]> appConfig.value?.inbounds
return <Inbound[]> Data().inbounds?? []
})
const inboundTags = computed((): string[] => {
const inboundTags = computed((): any[] => {
if (!inbounds.value) return []
return inbounds.value?.filter(i => i.tag != "" && Object.hasOwn(i,'users')).map(i => i.tag)
return inbounds.value?.filter(i => i.tag != "" && inboundWithUsers.includes(i.type)).map(i => { return { title: i.tag, value: i.id } })
})
const groups = computed((): string[] => {
@@ -430,102 +419,44 @@ const groupBy = [
const modal = ref({
visible: false,
index: -1,
id: 0,
data: "",
stats: false,
})
const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false))
const showModal = (id: number) => {
const index = id == -1 ? -1 : clients.value.findIndex(c => c.id == id)
modal.value.index = index
modal.value.data = index == -1 ? '' : JSON.stringify(clients.value[index])
modal.value.stats = index == -1 ? false : v2rayStats.value.users.includes(clients.value[index].name)
const showModal = async (id: number) => {
modal.value.id = id
modal.value.data = id == 0 ? '' : JSON.stringify(clients.value.findLast(o => o.id == id))
modal.value.visible = true
}
const closeModal = () => {
modal.value.visible = false
}
const saveModal = (data:any, stats:boolean) => {
const saveModal = async (data:any) => {
// Check duplicate name
const oldName = modal.value.index != -1 ? clients.value[modal.value.index].name : null
const oldName = modal.value.id > 0 ? clients.value.findLast(i => i.id == modal.value.id)?.name : null
if (data.name != oldName && clients.value.findIndex(c => c.name == data.name) != -1) {
push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('client.name')
})
return
}
if(modal.value.index == -1) {
clients.value.push(data)
} else {
clients.value[modal.value.index] = data
}
// Rebuild affected inbounds
buildInboundsUsers(data.inbounds)
// Rebuild links
data.links = updateLinks(data)
const clientInbounds = data.inbounds.length == 0 ? [] : await Data().loadInbounds(data.inbounds)
data.links = updateLinks(data, clientInbounds)
// Set Client Stats
const sIndex = v2rayStats.value.users.findIndex(i => i == data.name) // Find if new user exists
if (oldName != data.name) {
v2rayStats.value.users = v2rayStats.value.users.filter(item => item != oldName)
}
if (stats) {
// Add if dos not exist
if (data.name.length>0 && sIndex == -1) v2rayStats.value.users.push(data.name)
} else {
// Delete if exists
if (sIndex != -1) v2rayStats.value.users.splice(sIndex,1)
}
modal.value.visible = false
// save data
const success = await Data().save("clients", modal.value.id == 0 ? "new" : "edit", data)
if (success) modal.value.visible = false
}
const buildInboundsUsers = (inboundTags:string[]) => {
inboundTags.forEach(tag => {
const inbound_index = inbounds.value.findIndex(i => i.tag == tag)
if (inbound_index != -1){
const users = <any>[]
const newInbound = <InboundWithUser>inbounds.value[inbound_index]
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(tag))
inboundClients.forEach(c => {
// Remove flow in non tls VLESS
if (newInbound.type == InTypes.VLESS) {
const vlessInbound = <VLESS>newInbound
if (!vlessInbound.tls?.enabled || vlessInbound.transport?.type) delete(c.config?.vless?.flow)
}
users.push(c.config[newInbound.type])
})
newInbound.users = users
// Exceptions for Naive and ShadowTLSv3
if (users.length == 0){
if (newInbound.type == InTypes.Naive) {
newInbound.users = <any>[{}]
} else {
if (newInbound.type == InTypes.ShadowTLS){
const ssTls = <ShadowTLS>newInbound
if (ssTls.version == 3) newInbound.users = <any>[{}]
}
}
}
inbounds.value[inbound_index] = newInbound
}
})
}
const updateLinks = (c:Client):Link[] => {
const clientInbounds = <Inbound[]>inbounds.value.filter(i => c.inbounds.includes(i.tag))
const updateLinks = (c:Client, clientInbounds:Inbound[]):Link[] => {
const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{
const tlsConfig = <any>Data().tlsConfigs?.findLast((t:any) => t.inbounds.includes(i.tag))
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
const addrs = cData ? <any[]>cData.addrs : []
const uris = LinkUtil.linkGenerator(c,i, tlsConfig?.client?? {}, addrs)
const tls = i.tls_id && i.tls_id>0 ? Data().tlsConfigs?.findLast((t:any) => t.id == i.tls_id) : undefined
const uris = LinkUtil.linkGenerator(c,i, tls, i.addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
@@ -537,21 +468,10 @@ const updateLinks = (c:Client):Link[] => {
return links
}
const delClient = (id: number) => {
const clientIndex = clients.value.findIndex(c => c.id === id)
const oldData = createClient(clients.value[clientIndex])
// Delete stats if exists and will be orphaned
const tagCounts = clients.value.filter(i => i.name == oldData.name).length
const sIndex = v2rayStats.value.users.findIndex(i => i == oldData.name)
if (tagCounts == 1 && sIndex != -1){
v2rayStats.value.users.splice(sIndex,1)
}
clients.value.splice(clientIndex,1)
buildInboundsUsers(oldData.inbounds)
if (id>0) Data().delClient(id)
delOverlay.value[clientIndex] = false
const delClient = async (id: number) => {
const index = clients.value.findIndex(c => c.id === id)
const success = await Data().save("clients", "del", id)
if (success) delOverlay.value[index] = false
}
const qrcode = ref({
@@ -636,14 +556,12 @@ const closeBulk = () => {
addBulkModal.value = false
}
const saveBulk = (bulkClients: Client[], clientInbounds: string[], clientStats: boolean) => {
const saveBulk = async (bulkClients: Client[], clientInbounds: number[]) => {
const inboundData = clientInbounds.length == 0 ? [] : await Data().loadInbounds(clientInbounds)
bulkClients.forEach((c,c_index) => {
bulkClients[c_index].links = updateLinks(c)
bulkClients[c_index].links = updateLinks(c, inboundData)
})
clients.value.push(...bulkClients)
buildInboundsUsers(clientInbounds)
// Stats
if (clientStats) v2rayStats.value.users.push(...bulkClients.map(bc => bc.name))
closeBulk()
}
</script>
+166
View File
@@ -0,0 +1,166 @@
<template>
<EndpointVue
v-model="modal.visible"
:visible="modal.visible"
:id="modal.id"
:data="modal.data"
:tags="endpointTags"
@close="closeModal"
@save="saveModal"
/>
<Stats
v-model="stats.visible"
:visible="stats.visible"
:resource="stats.resource"
:tag="stats.tag"
@close="closeStats"
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>endpoints" :key="item.tag">
<v-card rounded="xl" elevation="5" min-width="200" :title="item.tag">
<v-card-subtitle style="margin-top: -20px;">
<v-row>
<v-col>{{ item.type }}</v-col>
</v-row>
</v-card-subtitle>
<v-card-text>
<v-row>
<v-col>{{ $t('in.addr') }}</v-col>
<v-col dir="ltr">
{{ item.address?.length>0 ? item.address[0] : '-' }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('in.port') }}</v-col>
<v-col dir="ltr">
{{ item.listen_port?? '-' }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('types.wg.peers') }}</v-col>
<v-col dir="ltr">
{{ item.peers.length?? '-' }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col dir="ltr">
<template v-if="onlines.includes(item.tag)">
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
</template>
<template v-else>-</template>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delOverlay[index] = true">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
</v-btn>
<v-overlay
v-model="delOverlay[index]"
contained
class="align-center justify-center"
>
<v-card :title="$t('actions.del')" rounded="lg">
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delEndpoint(item.tag)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts" setup>
import Data from '@/store/modules/data'
import EndpointVue from '@/layouts/modals/Endpoint.vue'
import Stats from '@/layouts/modals/Stats.vue'
import { Endpoint } from '@/types/endpoints';
import { computed, ref } from 'vue'
import { i18n } from '@/locales';
import { push } from 'notivue';
const endpoints = computed((): Endpoint[] => {
return <Endpoint[]> Data().endpoints
})
const endpointTags = computed((): any[] => {
return endpoints.value?.map((o:Endpoint) => o.tag)
})
const onlines = computed(() => {
return [...Data().onlines.inbound?? [], ...Data().onlines.outbound??[] ]
})
const modal = ref({
visible: false,
id: 0,
data: "",
})
let delOverlay = ref(new Array<boolean>)
const showModal = (id: number) => {
modal.value.id = id
modal.value.data = id == 0 ? '' : JSON.stringify(endpoints.value.findLast(o => o.id == id))
modal.value.visible = true
}
const closeModal = () => {
modal.value.visible = false
}
const saveModal = async (data:Endpoint) => {
// Check duplicate tag
const oldTag = modal.value.id > 0 ? endpoints.value.findLast(i => i.id == modal.value.id)?.tag : null
if (data.tag != oldTag && endpointTags.value.includes(data.tag)) {
push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag')
})
return
}
// save data
const success = await Data().save("endpoints", modal.value.id == 0 ? "new" : "edit", data)
if (success) modal.value.visible = false
}
const stats = ref({
visible: false,
resource: "endpoint",
tag: "",
})
const delEndpoint = async (tag: string) => {
const index = endpoints.value.findIndex(i => i.tag == tag)
const success = await Data().save("endpoints", "del", tag)
if (success) delOverlay.value[index] = false
}
const showStats = (tag: string) => {
stats.value.tag = tag
stats.value.visible = true
}
const closeStats = () => {
stats.value.visible = false
}
</script>
+84 -203
View File
@@ -2,10 +2,7 @@
<InboundVue
v-model="modal.visible"
:visible="modal.visible"
:index="modal.index"
:stats="modal.stats"
:data="modal.data"
:cData="modal.cData"
:id="modal.id"
:inTags="inTags"
:outTags="outTags"
:tlsConfigs="tlsConfigs"
@@ -21,7 +18,7 @@
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
</v-row>
<v-row>
@@ -48,22 +45,25 @@
<v-row>
<v-col>{{ $t('objects.tls') }}</v-col>
<v-col dir="ltr">
{{ Object.hasOwn(item,'tls') ? $t(item.tls?.enabled ? 'enable' : 'disable') : '-' }}
{{ item.tls_id > 0 ? $t('enable') : $t('disable') }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('pages.clients') }}</v-col>
<v-col dir="ltr">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="Object.hasOwn(item,'users')">
<span v-for="u in findInbounsUsers(item)">{{ u }}<br /></span>
</v-tooltip>
{{ Array.isArray(item.users) ? item.users.length : '-' }}
<template v-if="inboundWithUsers.includes(item.tag)">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="findInboundUsers(item.tag).length > 0">
<span v-for="u in findInboundUsers(item.tag)">{{ u }}<br /></span>
</v-tooltip>
{{ findInboundUsers(item.tag).length }}
</template>
<template v-else>-</template>
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col dir="ltr">
<template v-if="onlines[index]">
<template v-if="onlines.includes(item.tag)">
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
</template>
<template v-else>-</template>
@@ -72,7 +72,7 @@
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showModal(index)">
<v-btn icon="mdi-file-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
@@ -89,12 +89,12 @@
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delInbound(index)">{{ $t('yes') }}</v-btn>
<v-btn color="error" variant="outlined" @click="delInbound(item.id)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" v-if="v2rayStats.inbounds.includes(item.tag)">
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
@@ -108,9 +108,9 @@
import Data from '@/store/modules/data'
import InboundVue from '@/layouts/modals/Inbound.vue'
import Stats from '@/layouts/modals/Stats.vue'
import { Config, V2rayApiStats } from '@/types/config'
import { computed, ref } from 'vue'
import { InTypes, Inbound, InboundWithUser, ShadowTLS, VLESS } from '@/types/inbounds'
import { Config } from '@/types/config'
import { computed, onMounted, ref } from 'vue'
import { Inbound, inboundWithUsers } from '@/types/inbounds'
import { Client } from '@/types/clients'
import { Link, LinkUtil } from '@/plugins/link'
import { i18n } from '@/locales'
@@ -122,17 +122,13 @@ const appConfig = computed((): Config => {
})
const inbounds = computed((): Inbound[] => {
return <Inbound[]> appConfig.value.inbounds
return <Inbound[]> Data().inbounds
})
const tlsConfigs = computed((): any[] => {
return <any[]> Data().tlsConfigs
})
const inData = computed((): any[] => {
return <any[]> Data().inData
})
const inTags = computed((): string[] => {
return inbounds.value?.map(i => i.tag)
})
@@ -149,216 +145,101 @@ const onlines = computed(() => {
return Data().onlines.inbound ? inbounds.value.map(i => Data().onlines.inbound.includes(i.tag)) : []
})
const v2rayStats = computed((): V2rayApiStats => {
return <V2rayApiStats> appConfig.value.experimental?.v2ray_api.stats
})
const modal = ref({
visible: false,
index: -1,
data: "",
cData: "",
stats: false,
id: 0,
})
let delOverlay = ref(new Array<boolean>)
const showModal = (index: number) => {
modal.value.index = index
if (index == -1){
modal.value.data = ''
modal.value.cData = ''
modal.value.stats = false
} else {
modal.value.data = JSON.stringify(inbounds.value[index])
modal.value.stats = v2rayStats.value.inbounds.includes(inbounds.value[index].tag)
const inDataIndex = inData.value.findIndex(d => d.tag == inbounds.value[index].tag)
modal.value.cData = inDataIndex == -1 ? '' : JSON.stringify(inData.value[inDataIndex])
}
const showModal = (id: number) => {
modal.value.id = id
modal.value.visible = true
}
const closeModal = () => {
modal.value.visible = false
}
const saveModal = (data:Inbound, stats: boolean, tls_id: number, cData: any) => {
const saveModal = async (data:Inbound) => {
// Check duplicate tag
const oldTag = modal.value.index != -1 ? inbounds.value[modal.value.index].tag : null
if (data.tag != oldTag && inTags.value.includes(data.tag)) {
const oldInbound = modal.value.id > 0 ? inbounds.value.findLast(i => i.id == modal.value.id) : null
if (data.tag != oldInbound?.tag && inTags.value.includes(data.tag)) {
push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag')
})
return
}
if (cData.id != -1) {
cData.tag = data.tag
fillData(cData.outJson, data,tls_id>0 ? tlsConfigs.value.findLast(t => t.id == tls_id).client : {})
// Fill outjson
if (data.out_json){
fillData(data, data.tls_id > 0 ? tlsConfigs?.value.findLast((t:any) => t.id == data.tls_id) : null)
}
let userLinkDiff = []
// Update links
if (data.id > 0 && oldInbound != null) {
userLinkDiff = updateLinks(data,oldInbound)
}
// New or Edit
if (modal.value.index == -1) {
inbounds.value.push(data)
if (stats && data.tag.length>0) {
v2rayStats.value.inbounds.push(data.tag)
}
if (cData.id != -1){
inData.value.push(cData)
}
} else {
const oldTag = inbounds.value[modal.value.index].tag
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == data.tag) // Find if new tag exists
// Update tls preset
const oldTlsConfigIndex = tlsConfigs?.value.findIndex(t => t.inbounds?.includes(oldTag))
if (oldTlsConfigIndex != -1){
tlsConfigs.value[oldTlsConfigIndex].inbounds = tlsConfigs?.value[oldTlsConfigIndex].inbounds.filter((i:string) => i != oldTag)
}
if (oldTag != data.tag) {
v2rayStats.value.inbounds = v2rayStats.value.inbounds.filter(item => item != oldTag)
changeClientInboundsTag(oldTag,data.tag)
}
if (stats) {
// Add if dos not exist
if (data.tag.length>0 && sIndex == -1) v2rayStats.value.inbounds.push(data.tag)
} else {
// Delete if exists
if (sIndex != -1) v2rayStats.value.inbounds.splice(sIndex,1)
}
inbounds.value[modal.value.index] = data
const inDataIndex = inData.value.findIndex(indata => indata.tag == oldTag)
if (cData.id != -1) {
if (inDataIndex == -1){
inData.value.push(cData)
} else {
inData.value[inDataIndex] = cData
}
} else if (inDataIndex != -1) {
Data().delInData(inData.value[inDataIndex].id)
inData.value.splice(inDataIndex,1)
}
}
// Update tls preset
if (tls_id>0) {
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
tlsConfigs.value.sort()
}
if (Object.hasOwn(data,'users')) {
// Set users
data = buildInboundsUsers(data)
// Update links
updateLinks(data)
}
modal.value.visible = false
// save data
const success = await Data().save("inbounds", modal.value.id == 0 ? "new" : "edit", data, userLinkDiff)
if (success) modal.value.visible = false
}
const updateLinks = (i: any) => {
if(i.users){
const uClients = clients.value.filter(c => c.inbounds.includes(i.tag))
uClients.forEach((u:Client) => {
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => u.inbounds.includes(inb.tag))
const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{
const tlsClient = tlsConfigs?.value.findLast((t:any) => t.inbounds.includes(i.tag))?.client?? {}
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
const addrs = cData ? <any[]>cData.addrs : []
const uris = LinkUtil.linkGenerator(u,i, tlsClient, addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
}
})
let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...newLinks, ...links.filter(l => l.type != 'local')]
const updateLinks = (i: Inbound, o: Inbound): any[] => {
let diff = <any[]>[]
const uClients = clients.value.filter(c => c.inbounds.includes(i.id))
if (uClients.length == 0) return diff
u.links = links
if (inboundWithUsers.includes(o.type) && !inboundWithUsers.includes(i.type)){
// Remove old inbound links if new type does not support users
uClients.forEach((u:Client) => {
u.inbounds = u.inbounds.filter(i => i != o.id)
const otherLocalLinks = u.links.filter(l => l.type == 'local' && l.remark != o.tag)
let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...otherLocalLinks, ...links.filter(l => l.type != 'local')]
diff.push({ id: u.id, links: links, inbounds: u.inbounds })
})
} else if(inboundWithUsers.includes(i.type)){
// Add new inbound links if new type supports users
const tls = tlsConfigs?.value.findLast((t:any) => t.id == i.tls_id)
uClients.forEach((u:Client) => {
const otherLocalLinks = u.links.filter(l => l.type == 'local' && l.remark != i.tag)
const uris = LinkUtil.linkGenerator(u,i, tls, i.addrs)
let newLinks = <Link[]>[]
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
}
let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...otherLocalLinks, ...newLinks, ...links.filter(l => l.type != 'local')]
diff.push({ id: u.id, links: links, inbounds: u.inbounds })
})
}
return diff
}
const delInbound = (index: number) => {
const delInbound = async (id: number) => {
const index = inbounds.value.findIndex(i => i.id == id)
const inb = inbounds.value[index]
inbounds.value.splice(index,1)
const tag = inb.tag
if (Object.hasOwn(inb,'users')) {
const inbU = <InboundWithUser>inb
if (inbU.users && inbU.users.length>0){
inbU.users.forEach((u:any) => {
const c_index = clients.value.findIndex(c => u.username? u.username == c.name : u.name == c.name)
if (c_index != -1) {
clients.value[c_index].inbounds = clients.value[c_index].inbounds.filter((x:string) => x!=tag)
clients.value[c_index].links = clients.value[c_index].links.filter((x:any) => x.remark!=tag)
}
})
}
}
// Delete binded tls if exists
if (Object.hasOwn(inb,'tls')) {
const oldTlsConfigIndex = tlsConfigs?.value.findIndex(t => t.inbounds?.includes(inb.tag))
if (oldTlsConfigIndex != -1){
tlsConfigs.value[oldTlsConfigIndex].inbounds = tlsConfigs?.value[oldTlsConfigIndex].inbounds.filter((i:string) => i != inb.tag)
}
}
// Delete stats if exists and will be orphaned
const tagCounts = inbounds.value.filter(i => i.tag == inb.tag).length
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == inb.tag)
if (tagCounts == 1 && sIndex != -1){
v2rayStats.value.inbounds.splice(sIndex,1)
}
if (index < Data().oldData.config.inbounds.length){
Data().delInbound(index)
} else {
// Delete new inbound's inData if exists
const inDataIndex = Data().inData.findIndex((d:any) => d.tag == tag)
if (inDataIndex != -1) Data().inData.splice(inDataIndex, 1)
}
delOverlay.value[index] = false
}
const buildInboundsUsers = (inbound:any):Inbound => {
const users = <any>[]
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(inbound.tag))
inboundClients.forEach(c => {
// Remove flow in non tls VLESS
if (inbound.type == InTypes.VLESS) {
const vlessInbound = <VLESS>inbound
if (!vlessInbound.tls?.enabled || vlessInbound.transport?.type) delete(c.config?.vless?.flow)
}
users.push(c.config[inbound.type])
})
inbound.users = users
// Exceptions for Naive and ShadowTLSv3
if (users.length == 0){
if (inbound.type == InTypes.Naive){
inbound.users = <any>[{}]
} else {
if (inbound.type == InTypes.ShadowTLS){
const ssTls = <ShadowTLS>inbound
if (ssTls.version == 3) inbound.users = <any>[{}]
}
}
}
return <Inbound>inbound
}
const changeClientInboundsTag = (oldtag: string, newTag:string) => {
clients.value.forEach((c, c_index) => {
const inbound_index = c.inbounds.findIndex(i => i == oldtag)
if (inbound_index != -1) {
c.inbounds[inbound_index] = newTag
clients.value[c_index].inbounds = c.inbounds
}
let diff = <any[]>[]
// delete inbound in client table
const inboundClients = clients.value.filter(c => c.inbounds.includes(id))
inboundClients.forEach((c:Client) => {
c.inbounds = c.inbounds.filter((x:number) => x!=id)
c.links = c.links.filter((x:any) => x.remark!=tag)
diff.push({ id: c.id, links: c.links, inbounds: c.inbounds })
})
}
const findInbounsUsers = (inbound: InboundWithUser): string[] => {
if (inbound.users === null || !Array.isArray(inbound.users) || inbound.users.length == 0) return []
const users = inbound.users.map(user => "username" in user ? user.username : user.name)
return users
const success = await Data().save("inbounds", "del", tag, diff)
if (success) delOverlay.value[index] = false
}
const findInboundUsers = (i: Inbound): string[] => {
return clients.value.filter(c => c.inbounds.includes(i.id)).map(c => c.name)
}
const stats = ref({
+25 -56
View File
@@ -3,7 +3,6 @@
v-model="modal.visible"
:visible="modal.visible"
:id="modal.id"
:stats="modal.stats"
:data="modal.data"
:tags="outboundTags"
@close="closeModal"
@@ -18,7 +17,7 @@
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
</v-row>
<v-row>
@@ -51,7 +50,7 @@
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col dir="ltr">
<template v-if="onlines[index]">
<template v-if="onlines.includes(item.tag)">
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
</template>
<template v-else>-</template>
@@ -60,7 +59,7 @@
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showModal(index)">
<v-btn icon="mdi-file-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
@@ -77,12 +76,12 @@
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delOutbound(index)">{{ $t('yes') }}</v-btn>
<v-btn color="error" variant="outlined" @click="delOutbound(item.tag)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" v-if="v2rayStats.outbounds.includes(item.tag)">
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
@@ -96,79 +95,56 @@
import Data from '@/store/modules/data'
import OutboundVue from '@/layouts/modals/Outbound.vue'
import Stats from '@/layouts/modals/Stats.vue'
import { Config, V2rayApiStats } from '@/types/config';
import { Outbound } from '@/types/outbounds';
import { computed, ref } from 'vue'
import { i18n } from '@/locales';
import { push } from 'notivue';
const appConfig = computed((): Config => {
return <Config> Data().config
})
const outbounds = computed((): Outbound[] => {
return <Outbound[]> appConfig.value.outbounds
return <Outbound[]> Data().outbounds
})
const outboundTags = computed((): string[] => {
const outboundTags = computed((): any[] => {
return outbounds.value?.map((o:Outbound) => o.tag)
})
const onlines = computed(() => {
return Data().onlines.outbound ? outbounds.value.map(i => Data().onlines.outbound.includes(i.tag)) : []
})
const v2rayStats = computed((): V2rayApiStats => {
return <V2rayApiStats> appConfig.value.experimental?.v2ray_api.stats
return Data().onlines.outbound?? []
})
const modal = ref({
visible: false,
id: -1,
id: 0,
data: "",
stats: false,
})
let delOverlay = ref(new Array<boolean>)
const showModal = (id: number) => {
modal.value.id = id
modal.value.data = id == -1 ? '' : JSON.stringify(outbounds.value[id])
modal.value.stats = id == -1 ? false : v2rayStats.value.outbounds.includes(outbounds.value[id].tag)
modal.value.data = id == 0 ? '' : JSON.stringify(outbounds.value.findLast(o => o.id == id))
modal.value.visible = true
}
const closeModal = () => {
modal.value.visible = false
}
const saveModal = (data:Outbound, stats: boolean) => {
const saveModal = async (data:Outbound) => {
// Check duplicate tag
const oldTag = modal.value.id != -1 ? outbounds.value[modal.value.id].tag : null
const oldTag = modal.value.id > 0 ? outbounds.value.findLast(i => i.id == modal.value.id)?.tag : null
if (data.tag != oldTag && outboundTags.value.includes(data.tag)) {
push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag')
})
return
}
// New or Edit
if (modal.value.id == -1) {
outbounds.value.push(data)
if (stats && data.tag.length>0) {
v2rayStats.value.outbounds.push(data.tag)
}
} else {
const sIndex = v2rayStats.value.outbounds.findIndex(i => i == data.tag) // Find if new tag exists
if (stats) {
// Add if dos not exist
if (data.tag.length>0 && sIndex == -1) v2rayStats.value.outbounds.push(data.tag)
} else {
// Delete if exists
if (sIndex != -1) v2rayStats.value.outbounds.splice(sIndex,1)
}
outbounds.value[modal.value.id] = data
// save data
const success = await Data().save("outbounds", modal.value.id == 0 ? "new" : "edit", data)
if (!success) {
return
}
modal.value.visible = false
}
@@ -178,21 +154,10 @@ const stats = ref({
tag: "",
})
const delOutbound = (index: number) => {
const inb = outbounds.value[index]
outbounds.value.splice(index,1)
const tag = inb.tag
// Delete stats if exists and will be orphaned
const tagCounts = outbounds.value.filter(i => i.tag == inb.tag).length
const sIndex = v2rayStats.value.outbounds.findIndex(i => i == inb.tag)
if (tagCounts == 1 && sIndex != -1){
v2rayStats.value.outbounds.splice(sIndex,1)
}
if (index < Data().oldData.config.outbounds.length){
Data().delOutbound(index)
}
delOverlay.value[index] = false
const delOutbound = async (tag: string) => {
const index = outbounds.value.findIndex(i => i.tag == tag)
const success = await Data().save("outbounds", "del", tag)
if (success) delOverlay.value[index] = false
}
const showStats = (tag: string) => {
@@ -202,4 +167,8 @@ const showStats = (tag: string) => {
const closeStats = () => {
stats.value.visible = false
}
function awaitData() {
throw new Error('Function not implemented.');
}
</script>
+29 -5
View File
@@ -24,6 +24,9 @@
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showRuleModal(-1)" style="margin: 0 5px;">{{ $t('rule.add') }}</v-btn>
<v-btn color="primary" @click="showRulesetModal(-1)" style="margin: 0 5px;">{{ $t('ruleset.add') }}</v-btn>
<v-btn variant="outlined" color="warning" @click="saveConfig" :loading="loading" :disabled="stateChange">
{{ $t('actions.save') }}
</v-btn>
</v-col>
</v-row>
<v-row>
@@ -144,16 +147,37 @@
<script lang="ts" setup>
import Data from '@/store/modules/data'
import { computed, ref } from 'vue'
import { computed, ref, onMounted } from 'vue'
import RuleVue from '@/layouts/modals/Rule.vue'
import RulesetVue from '@/layouts/modals/Ruleset.vue'
import { Config } from '@/types/config'
import { logicalRule, ruleset } from '@/types/rules'
import { FindDiff } from '@/plugins/utils'
const oldConfig = ref({})
const loading = ref(false)
const appConfig = computed((): Config => {
return <Config> Data().config
})
onMounted(async () => {
oldConfig.value = JSON.parse(JSON.stringify(Data().config))
})
const stateChange = computed(() => {
return FindDiff.deepCompare(appConfig.value,oldConfig.value)
})
const saveConfig = async () => {
loading.value = true
const success = await Data().save("config", "set", appConfig.value)
if (success) {
oldConfig.value = JSON.parse(JSON.stringify(Data().config))
loading.value = false
}
}
const clients = computed((): string[] => {
return Data().clients.map((c:any) => c.name)
})
@@ -189,11 +213,11 @@ const rulesetTags = computed((): any[] => {
})
const outboundTags = computed((): string[] => {
return appConfig.value.outbounds?.map((o:any) => o.tag)
return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)]
})
const inboundTags = computed((): string[] => {
return appConfig.value.inbounds?.map((i:any) => i.tag)
return [...Data().inbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)]
})
let delRuleOverlay = ref(new Array<boolean>)
@@ -268,7 +292,7 @@ const draggedItemIndex = ref(null);
const onDragStart = (index: any) => {
draggedItemIndex.value = index;
};
}
const onDrop = (index: any) => {
if (draggedItemIndex.value !== null) {
@@ -278,5 +302,5 @@ const onDrop = (index: any) => {
rules.value.splice(index, 0, draggedItem);
draggedItemIndex.value = null;
}
};
}
</script>
+82 -71
View File
@@ -2,19 +2,19 @@
<TlsVue
v-model="modal.visible"
:visible="modal.visible"
:index="modal.index"
:id="modal.id"
:data="modal.data"
@close="closeModal"
@save="saveModal"
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>tlsConfigs" :key="item.id">
<v-card rounded="xl" elevation="5" min-width="200" :title="(item.id? item.id + '. ' : '*') + item.name">
<v-card rounded="xl" elevation="5" min-width="200" :title="item.name">
<v-card-subtitle style="margin-top: -20px;">
{{ item.server?.server_name?.length>0 ? item.server.server_name : "-" }}
</v-card-subtitle>
@@ -22,10 +22,13 @@
<v-row>
<v-col>{{ $t('pages.inbounds') }}</v-col>
<v-col dir="ltr">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds?.length>0">
<span v-for="i in item.inbounds">{{ i }}<br /></span>
</v-tooltip>
{{ item.inbounds?.length }}
<template v-if="tlsInbounds(item.id).length>0">
<v-tooltip activator="parent" dir="ltr" location="bottom">
<span v-for="i in tlsInbounds(item.id)">{{ i }}<br /></span>
</v-tooltip>
{{ tlsInbounds.length }}
</template>
<template v-else>-</template>
</v-col>
</v-row>
<v-row>
@@ -49,11 +52,11 @@
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showModal(index)">
<v-btn icon="mdi-file-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn v-if="item.inbounds?.length == 0" icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delOverlay[index] = true">
<v-btn v-if="tlsInbounds(item.id).length == 0" icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delOverlay[index] = true">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
</v-btn>
@@ -66,12 +69,12 @@
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delTls(index)">{{ $t('yes') }}</v-btn>
<v-btn color="error" variant="outlined" @click="delTls(item.id)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-content-duplicate" @click="clone(index)">
<v-btn icon="mdi-content-duplicate" @click="clone(item)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.clone')"></v-tooltip>
</v-btn>
@@ -85,8 +88,7 @@
import TlsVue from '@/layouts/modals/Tls.vue'
import Data from '@/store/modules/data'
import { computed, ref } from 'vue'
import { Config } from '@/types/config'
import { Inbound } from '@/types/inbounds'
import { Inbound, inboundWithUsers } from '@/types/inbounds'
import { Client } from '@/types/clients'
import { Link, LinkUtil } from '@/plugins/link'
import { fillData } from '@/plugins/outJson'
@@ -95,13 +97,13 @@ const tlsConfigs = computed((): any[] => {
return Data().tlsConfigs
})
const inbounds = computed((): any[] => {
return <any[]>(<Config>Data().config)?.inbounds
const inbounds = computed((): Inbound[] => {
return Data().inbounds
})
const inData = computed((): any[] => {
return <any[]> Data().inData
})
const tlsInbounds = (id: number): string[] => {
return inbounds.value.filter(i => i.tls_id == id).map(i => i.tag)
}
const clients = computed((): any[] => {
return <Client[]>Data().clients
@@ -109,21 +111,20 @@ const clients = computed((): any[] => {
const modal = ref({
visible: false,
index: -1,
id: 0,
data: "",
})
const delOverlay = ref(new Array<boolean>(tlsConfigs.value.length).fill(false))
const showModal = (index: number) => {
modal.value.index = index
modal.value.data = index == -1 ? '{}' : JSON.stringify(tlsConfigs.value[index])
const showModal = (id: number) => {
modal.value.id = id
modal.value.data = id == 0 ? '{}' : JSON.stringify(tlsConfigs.value.findLast(t => t.id == id))
modal.value.visible = true
}
const clone = (index: number) => {
let data = JSON.parse(JSON.stringify(tlsConfigs.value[index]))
const clone = (obj: any) => {
let data = JSON.parse(JSON.stringify(obj))
data.id = 0
data.inbounds = []
while (tlsConfigs.value.findIndex(t => t.name == data.name) != -1){
data.name += "-copy"
}
@@ -132,57 +133,67 @@ const clone = (index: number) => {
const closeModal = () => {
modal.value.visible = false
}
const saveModal = (data:any) => {
const saveModal = async (data:any) => {
let outJsons = <any[]>[]
let userLinks = <any[]>[]
// New or Edit
if (modal.value.index == -1) {
tlsConfigs.value.push(data)
} else {
tlsConfigs.value[modal.value.index] = data
inbounds?.value.filter(i => tlsConfigs.value[modal.value.index].inbounds.includes(i.tag)).forEach(i =>{
if (i.tls != undefined) i.tls = data.server
updateInData(i,data.client)
updateLinks(i,data.client)
})
}
modal.value.visible = false
}
const delTls = (index: number) => {
if (index < Data().oldData.tlsConfigs.length){
Data().delTls(tlsConfigs.value[index].id)
}
tlsConfigs.value.splice(index,1)
delOverlay.value[index] = false
}
const updateLinks = (i:any,tlsClient:any) => {
if(i.users){
const uClients = clients.value.filter(c => c.inbounds.includes(i.tag))
uClients.forEach((client:any) => {
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.includes(inb.tag))
const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
const addrs = cData ? <any[]>cData.addrs : []
const uris = LinkUtil.linkGenerator(client,i, tlsClient, addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
if (modal.value.id > 0) {
const inboundIds = inbounds.value.filter(i => i.tls_id == modal.value.id).map(i => i.id)
if (inboundIds.length > 0) {
const tlsInbounds = inboundIds.length == 0 ? [] : await Data().loadInbounds(inboundIds)
for (const inbound of tlsInbounds) {
// Fill outjson
if (inbound.out_json) {
fillData(inbound, data)
}
})
let links = client.links && client.links.length>0? client.links : <Link[]>[]
links = [...newLinks, ...links.filter((l:Link) => l.type != 'local')]
outJsons.push({tag: inbound.tag,out_jsons: inbound.out_json})
// Update links
const diff = updateLinks(inbound)
diff.forEach((d: any) => {
if (userLinks.findIndex(l => l.id == d.id) == -1) {
userLinks.push(d)
} else {
const index = userLinks.findIndex(l => l.id == d.id)
userLinks[index].links = d.links
}
})
}
}
client.links = links
}
const success = await Data().save("tls", data.id == 0 ? "new" : "edit", data, userLinks.length > 0 ? null: userLinks, outJsons.length > 0 ? null: outJsons)
if (success) modal.value.visible = false
}
const delTls = async (id: number) => {
const index = tlsConfigs.value.findIndex(t => t.id == id)
const success = await Data().save("tls", "del", id)
if (success) delOverlay.value[index] = false
}
const updateLinks = (i: Inbound): any[] => {
let diff = <any[]>[]
if(inboundWithUsers.includes(i.type) && i.id != 0){
const uClients = clients.value.filter(c => c.inbounds.includes(i.id))
const tlsClient = tlsConfigs?.value.findLast((t:any) => t.id == i.tls_id)
uClients.forEach((u:Client) => {
const otherLocalLinks = u.links.filter(l => l.type == 'local' && l.remark != i.tag)
const uris = LinkUtil.linkGenerator(u,i, tlsClient, i.addrs)
let newLinks = <Link[]>[]
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
}
let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...otherLocalLinks, ...newLinks, ...links.filter(l => l.type != 'local')]
u.links = links
diff.push({ id: u.id, links: links })
})
}
return diff
}
const updateInData = (i:any, c:any) => {
const inDataIndex = inData.value.findIndex(d => d.tag == i.tag)
if (inDataIndex != -1) {
fillData(inData.value[inDataIndex].outJson, i, c)
}
}
</script>