This commit is contained in:
Alireza Ahmadi
2024-06-06 08:24:08 +02:00
parent f136229539
commit c994f4b24a
26 changed files with 1335 additions and 81 deletions
+7 -2
View File
@@ -15,6 +15,7 @@ type APIHandler struct {
service.UserService service.UserService
service.ConfigService service.ConfigService
service.ClientService service.ClientService
service.TlsService
service.PanelService service.PanelService
service.StatsService service.StatsService
service.ServerService service.ServerService
@@ -159,7 +160,7 @@ func (a *APIHandler) getHandler(c *gin.Context) {
func (a *APIHandler) loadData(c *gin.Context) (string, error) { func (a *APIHandler) loadData(c *gin.Context) (string, error) {
var data string var data string
lu := c.Query("lu") lu := c.Query("lu")
isUpdated, err := a.ConfigService.CheckChnages(lu) isUpdated, err := a.ConfigService.CheckChanges(lu)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -176,11 +177,15 @@ func (a *APIHandler) loadData(c *gin.Context) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return "", err
}
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0]) subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
if err != nil { if err != nil {
return "", err return "", err
} }
data = fmt.Sprintf(`{"config": %s,"clients": %s,"subURI": "%s", "onlines": %s}`, string(*config), clients, subURI, onlines) data = fmt.Sprintf(`{"config": %s, "clients": %s, "tls": %s, "subURI": "%s", "onlines": %s}`, string(*config), clients, tlsConfigs, subURI, onlines)
} else { } else {
data = fmt.Sprintf(`{"onlines": %s}`, onlines) data = fmt.Sprintf(`{"onlines": %s}`, onlines)
} }
+1
View File
@@ -54,6 +54,7 @@ func InitDB(dbPath string) error {
err = db.AutoMigrate( err = db.AutoMigrate(
&model.Setting{}, &model.Setting{},
&model.Tls{},
&model.User{}, &model.User{},
&model.Stats{}, &model.Stats{},
&model.Client{}, &model.Client{},
+8
View File
@@ -8,6 +8,14 @@ type Setting struct {
Value string `json:"value" form:"value"` Value string `json:"value" form:"value"`
} }
type Tls struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" form:"name"`
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
Server json.RawMessage `json:"server" form:"server"`
Client json.RawMessage `json:"client" form:"client"`
}
type User struct { type User struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
+15 -2
View File
@@ -16,6 +16,7 @@ var LastUpdate int64
type ConfigService struct { type ConfigService struct {
ClientService ClientService
TlsService
singbox.Controller singbox.Controller
SettingService SettingService
} }
@@ -67,13 +68,19 @@ func (s *ConfigService) GetConfig() (*[]byte, error) {
func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error { func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error {
var err error var err error
var clientChanges, settingChanges, configChanges []model.Changes var clientChanges, tlsChanges, settingChanges, configChanges []model.Changes
if _, ok := changes["clients"]; ok { if _, ok := changes["clients"]; ok {
err = json.Unmarshal([]byte(changes["clients"]), &clientChanges) err = json.Unmarshal([]byte(changes["clients"]), &clientChanges)
if err != nil { if err != nil {
return err return err
} }
} }
if _, ok := changes["tls"]; ok {
err = json.Unmarshal([]byte(changes["tls"]), &tlsChanges)
if err != nil {
return err
}
}
if _, ok := changes["settings"]; ok { if _, ok := changes["settings"]; ok {
err = json.Unmarshal([]byte(changes["settings"]), &settingChanges) err = json.Unmarshal([]byte(changes["settings"]), &settingChanges)
if err != nil { if err != nil {
@@ -103,6 +110,12 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
return err return err
} }
} }
if len(tlsChanges) > 0 {
err = s.TlsService.Save(tx, tlsChanges)
if err != nil {
return err
}
}
if len(settingChanges) > 0 { if len(settingChanges) > 0 {
err = s.SettingService.Save(tx, settingChanges) err = s.SettingService.Save(tx, settingChanges)
if err != nil { if err != nil {
@@ -169,7 +182,7 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
// Log changes // Log changes
dt := time.Now().Unix() dt := time.Now().Unix()
allChanges := append(append(clientChanges, settingChanges...), configChanges...) allChanges := append(append(clientChanges, settingChanges...), append(configChanges, tlsChanges...)...)
for index := range allChanges { for index := range allChanges {
allChanges[index].DateTime = dt allChanges[index].DateTime = dt
allChanges[index].Actor = loginUser allChanges[index].Actor = loginUser
+49
View File
@@ -0,0 +1,49 @@
package service
import (
"encoding/json"
"s-ui/database"
"s-ui/database/model"
"gorm.io/gorm"
)
type TlsService struct {
}
func (s *TlsService) GetAll() (string, error) {
db := database.GetDB()
tlsConfig := []model.Tls{}
err := db.Model(model.Tls{}).Scan(&tlsConfig).Error
if err != nil {
return "", err
}
data, err := json.Marshal(tlsConfig)
if err != nil {
return "", err
}
return string(data), nil
}
func (s *TlsService) Save(tx *gorm.DB, changes []model.Changes) error {
var err error
for _, change := range changes {
tlsConfig := model.Tls{}
err = json.Unmarshal(change.Obj, &tlsConfig)
if err != nil {
return err
}
switch change.Action {
case "new":
err = tx.Create(&tlsConfig).Error
case "del":
err = tx.Where("id = ?", change.Index).Delete(model.Tls{}).Error
default:
err = tx.Save(tlsConfig).Error
}
if err != nil {
return err
}
}
return err
}
+252
View File
@@ -0,0 +1,252 @@
<template>
<v-card subtitle="ACME" style="background-color: inherit;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
</v-col>
<v-col cols="12" md="8" v-if="enabled">
<v-text-field
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
hide-details
v-model="domains">
</v-text-field>
</v-col>
</v-row>
<template v-if="enabled">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionDir">
<v-text-field
:label="$t('tls.acme.dataDir')"
hide-details
v-model="acme.data_directory">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionDefault">
<v-combobox
v-model="acme.default_server_name"
:items="acme.domain"
:label="$t('tls.acme.defaultDomain')"
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionEmail">
<v-text-field
:label="$t('email')"
hide-details
v-model="acme.email">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="optionChallenge">
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.acme.httpChallenge')" v-model="acme.disable_http_challenge" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.acme.tlsChallenge')" v-model="acme.disable_tls_alpn_challenge" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionPorts">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.acme.altHport')"
hide-details
type="number"
min=1
max="65532"
v-model.number="acme.alternative_http_port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.acme.altTport')"
hide-details
type="number"
min=1
max="65532"
v-model.number="acme.alternative_tls_port">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="optionProvider">
<v-col cols="12" sm="6" md="4">
<v-select
v-model="caProvider"
:items="providerList"
:label="$t('tls.acme.caProvider')"
hide-details
></v-select>
</v-col>
<v-col cols="12" md="8" v-if="caProvider == ''">
<v-text-field
:label="$t('tls.acme.customCa')"
hide-details
v-model="acme.provider">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="acme.external_account != undefined">
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Key ID"
hide-details
v-model="acme.external_account.key_id">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="MAC Key"
hide-details
v-model="acme.external_account.mac_key">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="acme.dns01_challenge != undefined">
<v-col cols="12" sm="6" md="4">
<v-select
:label="$t('tls.acme.dns01Provider')"
hide-details
:items="dnsProviders.map(d => d.provider)"
@update:model-value="acme.dns01_challenge = { provider: $event }"
v-model="acme.dns01_challenge.provider">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4"
v-for="item in dnsProviders.filter(d => d.provider == acme.dns01_challenge?.provider)[0]?.params"
:key="item">
<v-text-field
:label="item"
hide-details
v-model="acme.dns01_challenge[item]">
</v-text-field>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('tls.acme.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionDir" color="primary" :label="$t('tls.acme.dataDir')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDefault" color="primary" :label="$t('tls.acme.defaultDomain')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionEmail" color="primary" :label="$t('email')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionChallenge" color="primary" :label="$t('tls.acme.disableChallenges')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionPorts" color="primary" :label="$t('tls.acme.altPorts')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionProvider" color="primary" :label="$t('tls.acme.caProvider')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionExt" color="primary" :label="$t('tls.acme.extAcc')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDns01" color="primary" :label="$t('tls.acme.dns01')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</template>
</v-card>
</template>
<script lang="ts">
import { acme } from '@/types/inTls'
export default {
props: ['tls'],
data() {
return {
menu: false,
providerList: [
{ title: "Let's Encrypt", value: "letsencrypt" },
{ title: "ZeroSSL", value: "zerossl" },
{ title: "Custom", value: "" }
],
dnsProviders: [
{ provider: "cloudflare", params: [ "api_token" ] },
{ provider: "alidns", params: [ "access_key_id","access_key_secret","region_id" ] }
]
}
},
computed: {
acme() {
return <acme>this.$props.tls.acme
},
enabled: {
get() { return this.acme != undefined },
set(v: boolean) { this.$props.tls.acme = v ? { domain: [] } : undefined }
},
domains: {
get() { return this.acme?.domain ? this.acme.domain.join(',') : "" },
set(v: string) {
if(!v.endsWith(',')) {
this.acme.domain = v.length > 0 ? v.split(',') : []
}
}
},
caProvider: {
get() { return this.acme?.provider && ['letsencrypt','zerossl'].includes(this.acme.provider) ? this.acme?.provider : '' },
set(v: string) { this.acme.provider = ['letsencrypt','zerossl'].includes(v) ? v : 'https://' }
},
optionDir: {
get(): boolean { return this.acme?.data_directory != undefined },
set(v:boolean) { this.acme.data_directory = v ? '' : undefined }
},
optionDefault: {
get(): boolean { return this.acme?.default_server_name != undefined },
set(v:boolean) { this.acme.default_server_name = v ? this.domains.length>0 ? this.domains[0] : '' : undefined }
},
optionEmail: {
get(): boolean { return this.acme?.email != undefined },
set(v:boolean) { this.acme.email = v ? '' : undefined }
},
optionChallenge: {
get(): boolean { return this.acme?.disable_http_challenge != undefined || this.acme?.disable_tls_alpn_challenge != undefined },
set(v:boolean) {
if (v) {
this.acme.disable_http_challenge = false
this.acme.disable_tls_alpn_challenge = false
} else {
delete this.acme.disable_http_challenge
delete this.acme.disable_tls_alpn_challenge
}
}
},
optionPorts: {
get(): boolean { return this.acme?.alternative_http_port != undefined || this.acme?.alternative_tls_port != undefined },
set(v:boolean) {
if (v) {
this.acme.alternative_http_port = 80
this.acme.alternative_tls_port = 443
} else {
delete this.acme.alternative_http_port
delete this.acme.alternative_tls_port
}
}
},
optionProvider: {
get(): boolean { return this.acme?.provider != undefined },
set(v:boolean) { this.acme.provider = v ? 'letsencrypt' : undefined }
},
optionExt: {
get(): boolean { return this.acme?.external_account != undefined },
set(v:boolean) { this.acme.external_account = v ? { key_id: '', mac_key: '' } : undefined }
},
optionDns01: {
get(): boolean { return this.acme?.dns01_challenge != undefined },
set(v:boolean) { this.acme.dns01_challenge = v ? { provider: 'cloudflare' } : undefined }
},
}
}
</script>
+96
View File
@@ -0,0 +1,96 @@
<template>
<v-card subtitle="ECH" style="background-color: inherit;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
</v-col>
</v-row>
<template v-if="enabled">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Post-Quantum Schemes" v-model="ech.pq_signature_schemes_enabled" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Disable Adaptive Size" v-model="ech.dynamic_record_sizing_disabled" hide-details></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="useEchPath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="delete ech.key"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="delete ech.key_path"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row v-if="useEchPath == 0">
<v-col cols="12" sm="6">
<v-text-field
:label="$t('tls.keyPath')"
hide-details
v-model="ech.key_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.key')"
hide-details
v-model="echKeyText">
</v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.cert')"
hide-details
v-model="echConfigText">
</v-textarea>
</v-col>
</v-row>
</template>
</v-card>
</template>
<script lang="ts">
import { ech } from '@/types/inTls'
export default {
props: ['iTls','oTls'],
data() {
return {
useEchPath: 0
}
},
computed: {
ech() {
return <ech>this.$props.iTls.ech
},
enabled: {
get() { return this.ech?.enabled?? false },
set(v: boolean) {
this.$props.iTls.ech = v ? { enabled: true } : undefined
this.$props.oTls.ech = v ? {} : undefined
}
},
echKeyText: {
get(): string { return this.ech?.key ? this.ech.key.join('\n') : '' },
set(newValue:string) { this.ech.key = newValue.split('\n') }
},
echConfigText: {
get(): string { return this.oTls.ech?.config ? this.oTls.ech.config.join('\n') : '' },
set(newValue:string) { this.oTls.ech.config = newValue.split('\n') }
},
}
}
</script>
+38 -12
View File
@@ -1,11 +1,20 @@
<template> <template>
<v-card :subtitle="$t('objects.tls')"> <v-card :subtitle="$t('objects.tls')">
<v-row v-if="tlsOptional"> <v-row>
<v-col cols="auto"> <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-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.enabled">
<v-select
hide-details
label="Preset"
:items="tlsItems"
@update:model-value="changeTlsItem($event)"
v-model="tlsId">
</v-select>
</v-col>
</v-row> </v-row>
<template v-if="tls.enabled"> <template v-if="tls.enabled && tlsId == 0">
<v-row> <v-row>
<v-col cols="auto"> <v-col cols="auto">
<v-btn-toggle v-model="usePath" <v-btn-toggle v-model="usePath"
@@ -103,7 +112,7 @@
</v-col> </v-col>
</v-row> </v-row>
</template> </template>
<v-card-actions v-if="tls.enabled"> <v-card-actions v-if="tls.enabled && tlsId == 0">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled"> <v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
@@ -136,11 +145,11 @@
<script lang="ts"> <script lang="ts">
import { iTls, defaultInTls } from '@/types/inTls' import { iTls, defaultInTls } from '@/types/inTls'
export default { export default {
props: ['inbound'], props: ['inbound', 'tlsConfigs', 'tls_id'],
data() { data() {
return { return {
menu: false, menu: false,
usePath: 0, usePath: this.$props.inbound.tls.key == undefined ? 0 : 1,
defaults: defaultInTls, defaults: defaultInTls,
alpn: [ alpn: [
{ title: "H3", value: 'h3' }, { title: "H3", value: 'h3' },
@@ -173,8 +182,15 @@ export default {
tls(): iTls { tls(): iTls {
return <iTls> this.$props.inbound.tls return <iTls> this.$props.inbound.tls
}, },
tlsItems(): any[] {
return [ { title: '', 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: { tlsEnable: {
get() { return Object.hasOwn(this.$props.inbound.tls, 'enabled') ? this.tls.enabled : false }, get() { return this.tls.enabled?? false },
set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} } set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} }
}, },
tlsOptional(): boolean { tlsOptional(): boolean {
@@ -190,23 +206,33 @@ export default {
}, },
optionSNI: { optionSNI: {
get(): boolean { return this.tls.server_name != undefined }, get(): boolean { return this.tls.server_name != undefined },
set(v:boolean) { this.$props.inbound.tls.server_name = v ? '' : undefined } set(v:boolean) { this.tls.server_name = v ? '' : undefined }
}, },
optionALPN: { optionALPN: {
get(): boolean { return this.tls.alpn != undefined }, get(): boolean { return this.tls.alpn != undefined },
set(v:boolean) { this.$props.inbound.tls.alpn = v ? defaultInTls.alpn : undefined } set(v:boolean) { this.tls.alpn = v ? defaultInTls.alpn : undefined }
}, },
optionMinV: { optionMinV: {
get(): boolean { return this.tls.min_version != undefined }, get(): boolean { return this.tls.min_version != undefined },
set(v:boolean) { this.$props.inbound.tls.min_version = v ? defaultInTls.min_version : undefined } set(v:boolean) { this.tls.min_version = v ? defaultInTls.min_version : undefined }
}, },
optionMaxV: { optionMaxV: {
get(): boolean { return this.tls.max_version != undefined }, get(): boolean { return this.tls.max_version != undefined },
set(v:boolean) { this.$props.inbound.tls.max_version = v ? defaultInTls.max_version : undefined } set(v:boolean) { this.tls.max_version = v ? defaultInTls.max_version : undefined }
}, },
optionCS: { optionCS: {
get(): boolean { return this.tls.cipher_suites != undefined }, get(): boolean { return this.tls.cipher_suites != undefined },
set(v:boolean) { this.$props.inbound.tls.cipher_suites = v ? defaultInTls.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 }
}
} }
} }
} }
@@ -39,7 +39,7 @@
<v-row v-if="Inbound.handshake_for_server_name != undefined"> <v-row v-if="Inbound.handshake_for_server_name != undefined">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
:label="$t('types.shdwTls.adHS')" :label="$t('types.shdwTls.addHS')"
hide-details hide-details
append-icon="mdi-plus" append-icon="mdi-plus"
@click:append="addHandshakeServer()" @click:append="addHandshakeServer()"
+2 -2
View File
@@ -32,11 +32,11 @@ const saveChanges = () => {
} }
const oldData = computed((): any => { const oldData = computed((): any => {
return {config: store.oldData.config, clients: store.oldData.clients} return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs}
}) })
const newData = computed((): any => { const newData = computed((): any => {
return {config: store.config, clients: store.clients} return {config: store.config, clients: store.clients, tls: store.tlsConfigs}
}) })
const stateChange = computed((): any => { const stateChange = computed((): any => {
+1
View File
@@ -54,6 +54,7 @@ const menu = [
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' }, { title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' }, { title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
{ title: 'pages.rules', icon: 'mdi-routes', path: '/rules' }, { 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' }, { title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' },
{ title: 'pages.admins', icon: 'mdi-account-tie', path: '/admins' }, { title: 'pages.admins', icon: 'mdi-account-tie', path: '/admins' },
{ title: 'pages.settings', icon: 'mdi-cog', path: '/settings' }, { title: 'pages.settings', icon: 'mdi-cog', path: '/settings' },
+5 -4
View File
@@ -31,7 +31,7 @@
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" /> <TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
<Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" /> <Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" />
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" :id="id" /> <Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" :id="id" />
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" /> <InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="tls_id" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" /> <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-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-card-text> </v-card-text>
@@ -57,7 +57,6 @@
</v-dialog> </v-dialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import { InTypes, createInbound } from '@/types/inbounds' import { InTypes, createInbound } from '@/types/inbounds'
import Listen from '@/components/Listen.vue' import Listen from '@/components/Listen.vue'
@@ -75,7 +74,7 @@ import RandomUtil from '@/plugins/randomUtil'
import Multiplex from '@/components/Multiplex.vue' import Multiplex from '@/components/Multiplex.vue'
import Transport from '@/components/Transport.vue' import Transport from '@/components/Transport.vue'
export default { export default {
props: ['visible', 'data', 'id', 'stats', 'inTags', 'outTags'], props: ['visible', 'data', 'id', 'stats', 'inTags', 'outTags', 'tlsConfigs'],
emits: ['close', 'save'], emits: ['close', 'save'],
data() { data() {
return { return {
@@ -84,6 +83,7 @@ export default {
loading: false, loading: false,
inTypes: InTypes, inTypes: InTypes,
inboundStats: false, inboundStats: false,
tls_id: { value: 0 },
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks], HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
} }
}, },
@@ -92,6 +92,7 @@ export default {
if (this.$props.id != -1) { if (this.$props.id != -1) {
const newData = JSON.parse(this.$props.data) const newData = JSON.parse(this.$props.data)
this.inbound = createInbound(newData.type, newData) this.inbound = createInbound(newData.type, newData)
this.tls_id.value = this.$props.tlsConfigs?.findLast((t:any) => t.inbounds?.includes(this.inbound.tag))?.id?? 0
this.title = "edit" this.title = "edit"
} }
else { else {
@@ -114,7 +115,7 @@ export default {
}, },
saveChanges() { saveChanges() {
this.loading = true this.loading = true
this.$emit('save', this.inbound, this.inboundStats) this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value)
this.loading = false this.loading = false
}, },
}, },
+1 -1
View File
@@ -18,7 +18,7 @@
</v-row> </v-row>
<v-row v-for="l in clientLinks"> <v-row v-for="l in clientLinks">
<v-col style="text-align: center;" @click="copyToClipboard(l.uri)"> <v-col style="text-align: center;" @click="copyToClipboard(l.uri)">
<v-chip>{{ l.remark }}</v-chip><br /> <v-chip>{{ l.remark?? "-" }}</v-chip><br />
<QrcodeVue :value="l.uri" :size="300" :margin="1" style="border-radius: 1rem;" /> <QrcodeVue :value="l.uri" :size="300" :margin="1" style="border-radius: 1rem;" />
</v-col> </v-col>
</v-row> </v-row>
+442
View File
@@ -0,0 +1,442 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.tls') }}
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-card class="rounded-lg">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('client.name')"
hide-details
v-model="tls.name">
</v-text-field>
</v-col>
<v-col align="end">
<v-btn-toggle v-model="tlsType"
class="rounded-xl"
density="compact"
variant="outlined"
@update:model-value="changeTlsType"
shaped
mandatory>
<v-btn>TLS</v-btn>
<v-btn>Reality</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="inTls.server_name != undefined">
<v-text-field
label="SNI"
hide-details
v-model="inTls.server_name">
</v-text-field>
</v-col>
<template v-if="tlsType == 0">
<v-col cols="12" sm="6" md="4" v-if="inTls.min_version">
<v-select
hide-details
:label="$t('tls.minVer')"
:items="tlsVersions"
v-model="inTls.min_version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="inTls.max_version">
<v-select
hide-details
:label="$t('tls.maxVer')"
:items="tlsVersions"
v-model="inTls.max_version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="inTls.alpn">
<v-select
hide-details
label="ALPN"
multiple
:items="alpn"
v-model="inTls.alpn">
</v-select>
</v-col>
<v-col cols="12" md="8" v-if="inTls.cipher_suites != undefined">
<v-select
hide-details
:label="$t('tls.cs')"
multiple
:items="cipher_suites"
v-model="inTls.cipher_suites">
</v-select>
</v-col>
</template>
</v-row>
<template v-if="tlsType == 0">
<v-row>
<v-col>
<v-btn-toggle v-model="usePath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="inTls.key=undefined; inTls.certificate=undefined"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="inTls.key_path=undefined; inTls.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">
<v-text-field
:label="$t('tls.certPath')"
hide-details
v-model="inTls.certificate_path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
:label="$t('tls.keyPath')"
hide-details
v-model="inTls.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="outTls.utls != undefined">
<v-select
hide-details
label="Fingerprint"
:items="fingerprints"
v-model="outTls.utls.fingerprint">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.disableSni')" v-model="disableSni" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.insecure')" v-model="insecure" hide-details></v-switch>
</v-col>
</v-row>
</template>
<template v-if="outTls.reality && inTls.reality">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.shdwTls.hs')"
hide-details
v-model="inTls.reality.handshake.server">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.port')"
type="number"
min="0"
hide-details
v-model="server_port">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-text-field
:label="$t('tls.privKey')"
hide-details
v-model="inTls.reality.private_key">
</v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
:label="$t('tls.pubKey')"
hide-details
v-model="outTls.reality.public_key">
</v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-text-field
label="Short IDs"
hide-details
v-model="short_id">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionTime">
<v-text-field
label="Max Time Diference"
type="number"
min="1"
:suffix="$t('date.m')"
hide-details
v-model="max_time">
</v-text-field>
</v-col>
</v-row>
</template>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('tls.options') }}</v-btn>
</template>
<v-card>
<v-list>
<template v-if="tlsType == 0">
<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-item>
<v-switch v-model="optionFP" color="primary" label="UTLS" hide-details></v-switch>
</v-list-item>
</template>
<template v-else>
<v-list-item>
<v-switch v-model="optionTime" color="primary" label="Max Time Difference" hide-details></v-switch>
</v-list-item>
</template>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
<AcmeVue :tls="inTls" />
<EchVue :iTls="inTls" :oTls="outTls" />
</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 { iTls, defaultInTls } from '@/types/inTls'
import { oTls, defaultOutTls } from '@/types/outTls'
import AcmeVue from '@/components/Acme.vue'
import EchVue from '@/components/Ech.vue'
export default {
props: ['visible', 'data', 'index'],
emits: ['close', 'save'],
data() {
return {
tls: { id: -1, name: '', inbounds: [], server: <iTls>{ enabled: true }, client: <oTls>{} },
title: "add",
loading: false,
menu: false,
tlsType: 0,
usePath: 0,
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" }
],
fingerprints: [
{ title: "Chrome", value: "chrome" },
{ title: "Chrome PSK", value: "chrome_psk" },
{ title: "Chrome PSK Shuffle", value: "chrome_psk_shuffle" },
{ title: "Chrome Padding PSK Shuffle", value: "chrome_padding_psk_shuffle" },
{ title: "Chrome Post-Quantum", value: "chrome_pq" },
{ title: "Chrome Post-Quantum PSK", value: "chrome_pq_psk" },
{ title: "Firefox", value: "firefox" },
{ title: "Microsoft Edge", value: "edge" },
{ title: "Apple Safari", value: "safari" },
{ title: "360", value: "360" },
{ title: "QQ", value: "qq" },
{ title: "Apple IOS", value: "ios" },
{ title: "Android", value: "android" },
{ title: "Random", value: "random" },
{ title: "Randomized", value: "randomized" },
]
}
},
methods: {
updateData() {
if (this.$props.index != -1) {
const newData = JSON.parse(this.$props.data)
this.tls = newData
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.usePath = 0
this.title = "add"
}
},
changeTlsType(){
if (this.tlsType) {
this.tls.server = <iTls>{ enabled: true, reality: { enabled: true, handshake: { server_port: 443 } }, server_name: "" }
this.tls.client = <oTls>{ reality: { public_key: "" } }
} else {
this.tls.server = <iTls>{ enabled: true }
this.tls.client = <oTls>{}
}
},
closeModal() {
this.updateData() // reset
this.$emit('close')
},
saveChanges() {
this.loading = true
this.$emit('save', this.tls)
this.loading = false
},
},
computed: {
inTls(): iTls {
return <iTls> this.tls.server
},
outTls(): oTls {
return <oTls> this.tls.client
},
certText: {
get(): string { return this.inTls.certificate ? this.inTls.certificate.join('\n') : '' },
set(v:string) { this.inTls.certificate = v.split('\n') }
},
keyText: {
get(): string { return this.inTls.key ? this.inTls.key.join('\n') : '' },
set(v:string) { this.inTls.key = v.split('\n') }
},
disableSni: {
get() { return this.outTls.disable_sni ?? false },
set(v: boolean) { this.outTls.disable_sni = v ? true : undefined }
},
insecure: {
get() { return this.outTls.insecure ?? false },
set(v: boolean) { this.outTls.insecure = v ? true : undefined }
},
server_port: {
get() { return this.inTls.reality?.handshake?.server_port ? this.inTls.reality.handshake.server_port : 443 },
set(v: any) {
if (this.inTls.reality){
this.inTls.reality.handshake.server_port = v.length == 0 || v == 0 ? 443 : parseInt(v)
}
}
},
short_id: {
get() { return this.inTls.reality?.short_id ? this.inTls.reality.short_id.join(',') : undefined },
set(v: string) {
if (this.inTls.reality){
this.inTls.reality.short_id = v.length > 0 ? v.split(',') : []
}
}
},
max_time: {
get() { return this.inTls?.reality?.max_time_difference ? this.inTls.reality.max_time_difference.replace('m','') : 1 },
set(v: number) {
if (this.inTls.reality){
this.inTls.reality.max_time_difference = v > 0 ? v + 'm' : '1m'
}
}
},
optionSNI: {
get(): boolean { return this.inTls.server_name != undefined },
set(v:boolean) { this.inTls.server_name = v ? '' : undefined }
},
optionALPN: {
get(): boolean { return this.inTls.alpn != undefined },
set(v:boolean) { this.inTls.alpn = v ? defaultInTls.alpn : undefined }
},
optionMinV: {
get(): boolean { return this.inTls.min_version != undefined },
set(v:boolean) { this.inTls.min_version = v ? defaultInTls.min_version : undefined }
},
optionMaxV: {
get(): boolean { return this.inTls.max_version != undefined },
set(v:boolean) { this.inTls.max_version = v ? defaultInTls.max_version : undefined }
},
optionCS: {
get(): boolean { return this.inTls.cipher_suites != undefined },
set(v:boolean) { this.inTls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
},
optionFP: {
get(): boolean { return this.outTls.utls != undefined },
set(v:boolean) { this.outTls.utls = v ? defaultOutTls.utls : undefined }
},
optionEch: {
get(): boolean { return this.outTls.ech != undefined },
set(v:boolean) { this.outTls.ech = v ? defaultOutTls.ech : undefined }
},
optionTime: {
get(): boolean { return this.inTls?.reality?.max_time_difference != undefined },
set(v:boolean) { if (this.inTls.reality) this.inTls.reality.max_time_difference = v ? "1m" : undefined }
}
},
watch: {
visible(v) {
if (v) {
this.updateData()
}
},
},
components: { AcmeVue, EchVue }
}
</script>
+19
View File
@@ -21,6 +21,7 @@ export default {
invalidLogin: "Invalid Login!", invalidLogin: "Invalid Login!",
online: "Online", online: "Online",
version: "Version", version: "Version",
email: "Email",
commaSeparated: "(comma separated)", commaSeparated: "(comma separated)",
error: { error: {
dplData: "Duplicate Data", dplData: "Duplicate Data",
@@ -32,6 +33,7 @@ export default {
outbounds: "Outbounds", outbounds: "Outbounds",
clients: "Clients", clients: "Clients",
rules: "Rules", rules: "Rules",
tls: "TLS Settings",
basics: "Basics", basics: "Basics",
admins: "Admins", admins: "Admins",
settings: "Settings", settings: "Settings",
@@ -326,9 +328,26 @@ export default {
minVer: "Minimum Version", minVer: "Minimum Version",
maxVer: "Maximum Version", maxVer: "Maximum Version",
cs: "Cipher suits", cs: "Cipher suits",
privKey: "Private Key",
pubKey: "Public Key", pubKey: "Public Key",
disableSni: "Disable SNI", disableSni: "Disable SNI",
insecure: "Allow Insecure", insecure: "Allow Insecure",
acme: {
options: "ACME Options",
dataDir: "Data Directory",
defaultDomain: "Default Domain",
disableChallenges: "Disable Challenges",
httpChallenge: "Disable HTTP Challenge",
tlsChallenge: "Disable TLS Challenge",
altPorts: "Alternative Ports",
altHport: "Alternative HTTP Port",
altTport: "Alternative TLS Port",
caProvider: "CA Provider",
customCa: "Custom CA Provider",
extAcc: "External Account",
dns01: "DNS01 Challenge",
dns01Provider: "DNS01 Challenge Provider",
},
}, },
stats: { stats: {
upload: "Upload", upload: "Upload",
+19
View File
@@ -21,6 +21,7 @@ export default {
invalidLogin: "ورود نامعتبر!", invalidLogin: "ورود نامعتبر!",
online: "آنلاین", online: "آنلاین",
version: "نسخه", version: "نسخه",
email: "ایمیل",
commaSeparated: "(جداشده با کاما)", commaSeparated: "(جداشده با کاما)",
error: { error: {
dplData: "داده تکراری", dplData: "داده تکراری",
@@ -32,6 +33,7 @@ export default {
outbounds: "خروجی‌ها", outbounds: "خروجی‌ها",
clients: "کاربران", clients: "کاربران",
rules: "قوانین", rules: "قوانین",
tls: "رمزنگاری‌ها",
basics: "ترازها", basics: "ترازها",
admins: "ادمین‌ها", admins: "ادمین‌ها",
settings: "پیکربندی", settings: "پیکربندی",
@@ -325,9 +327,26 @@ export default {
minVer: "کمینه نسخه", minVer: "کمینه نسخه",
maxVer: "بیشینه نسخه", maxVer: "بیشینه نسخه",
cs: "مدل‌های رمزنگاری", cs: "مدل‌های رمزنگاری",
privKey: "کلید خصوصی",
pubKey: "کلید عمومی", pubKey: "کلید عمومی",
disableSni: "غیرفعال‌سازی SNI", disableSni: "غیرفعال‌سازی SNI",
insecure: "تایید ارتباط ناامن", insecure: "تایید ارتباط ناامن",
acme: {
options: "گزینه‌های ACME",
dataDir: "مسیر داده‌ها",
defaultDomain: "دامنه پیش‌فرض",
disableChallenges: "بستن چالش‌ها",
httpChallenge: "بستن چالش HTTP",
tlsChallenge: "بستن چالش TLS",
altPorts: "پورت‌های جایگزین",
altHport: "پورت جایگزین HTTP",
altTport: "پورت جایگزین TLS",
caProvider: "فراهم کننده گواهی",
customCa: "فراهم کننده دیگر",
extAcc: "حساب خارجی",
dns01: "چالش DNS01",
dns01Provider: "فراهم کننده چالش DNS01",
},
}, },
stats: { stats: {
upload: "آپلود", upload: "آپلود",
+19
View File
@@ -21,6 +21,7 @@ export default {
invalidLogin: "Đăng nhập không hợp lệ!", invalidLogin: "Đăng nhập không hợp lệ!",
online: "Trực tuyến", online: "Trực tuyến",
version: "Phiên bản", version: "Phiên bản",
email: "Email",
commaSeparated: "(được phân tách bằng dấu phẩy)", commaSeparated: "(được phân tách bằng dấu phẩy)",
error: { error: {
dplData: "Dữ liệu trùng lặp", dplData: "Dữ liệu trùng lặp",
@@ -32,6 +33,7 @@ export default {
outbounds: "Đầu ra", outbounds: "Đầu ra",
clients: "Khách hàng", clients: "Khách hàng",
rules: "Quy tắc", rules: "Quy tắc",
tls: "Cài đặt TLS",
basics: "Cơ bản", basics: "Cơ bản",
admins: "Quản trị viên", admins: "Quản trị viên",
settings: "Cài đặt", settings: "Cài đặt",
@@ -327,9 +329,26 @@ export default {
minVer: "Phiên bản Tối thiểu", minVer: "Phiên bản Tối thiểu",
maxVer: "Phiên bản Tối đa", maxVer: "Phiên bản Tối đa",
cs: "Các bộ mã hóa", cs: "Các bộ mã hóa",
privKey: "Khóa riêng",
pubKey: "Khóa Công khai", pubKey: "Khóa Công khai",
disableSni: "Tắt SNI", disableSni: "Tắt SNI",
insecure: "Cho phép Không an toàn", insecure: "Cho phép Không an toàn",
acme: {
options: "Tùy chọn ACME",
dataDir: "Thư mục Dữ liệu",
defaultDomain: "Tên miền Mặc định",
disableChallenges: "Vô hiệu hóa Thách thức",
httpChallenge: "Vô hiệu hóa Thách thức HTTP",
tlsChallenge: "Vô hiệu hóa Thách thức TLS",
altPorts: "Cổng Thay thế",
altHport: "Cổng HTTP Thay thế",
altTport: "Cổng TLS Thay thế",
caProvider: "Nhà cung cấp CA",
customCa: "Nhà cung cấp CA Tùy chỉnh",
extAcc: "Tài khoản Bên ngoài",
dns01: "Thách thức DNS01",
dns01Provider: "Nhà cung cấp Thách thức DNS01"
},
}, },
stats: { stats: {
upload: "Tải lên", upload: "Tải lên",
+19
View File
@@ -21,6 +21,7 @@ export default {
invalidLogin: "登录无效!", invalidLogin: "登录无效!",
online: "在线", online: "在线",
version: "版本", version: "版本",
email: "电子邮件",
commaSeparated: "(逗号分隔)", commaSeparated: "(逗号分隔)",
error: { error: {
dplData: "重复数据", dplData: "重复数据",
@@ -32,6 +33,7 @@ export default {
outbounds: "出站管理", outbounds: "出站管理",
clients: "用户管理", clients: "用户管理",
rules: "路由列表", rules: "路由列表",
tls: "TLS 设置",
basics: "基础信息", basics: "基础信息",
admins: "管理员", admins: "管理员",
settings: "设置", settings: "设置",
@@ -327,9 +329,26 @@ export default {
minVer: "最低版本", minVer: "最低版本",
maxVer: "最高版本", maxVer: "最高版本",
cs: "密码套件", cs: "密码套件",
privKey: "私钥",
pubKey: "公钥", pubKey: "公钥",
disableSni: "禁用SNI", disableSni: "禁用SNI",
insecure: "允许不安全", insecure: "允许不安全",
acme: {
options: "ACME 选项",
dataDir: "数据目录",
defaultDomain: "默认域名",
disableChallenges: "禁用挑战",
httpChallenge: "禁用 HTTP 挑战",
tlsChallenge: "禁用 TLS 挑战",
altPorts: "替代端口",
altHport: "替代 HTTP 端口",
altTport: "替代 TLS 端口",
caProvider: "CA 提供商",
customCa: "自定义 CA 提供商",
extAcc: "外部账户",
dns01: "DNS01 挑战",
dns01Provider: "DNS01 挑战提供商"
},
}, },
stats: { stats: {
upload: "上传", upload: "上传",
+19
View File
@@ -22,6 +22,7 @@ export default {
invalidLogin: "登錄無效!", invalidLogin: "登錄無效!",
online: "在線", online: "在線",
version: "版本", version: "版本",
email: "電子郵件",
commaSeparated: "(逗號分隔)", commaSeparated: "(逗號分隔)",
error: { error: {
dplData: "重複數據", dplData: "重複數據",
@@ -33,6 +34,7 @@ export default {
outbounds: "出站管理", outbounds: "出站管理",
clients: "用戶管理", clients: "用戶管理",
rules: "路由列表", rules: "路由列表",
tls: "TLS 設置",
basics: "基礎信息", basics: "基礎信息",
admins: "管理員", admins: "管理員",
settings: "設置", settings: "設置",
@@ -328,9 +330,26 @@ export default {
minVer: "最低版本", minVer: "最低版本",
maxVer: "最高版本", maxVer: "最高版本",
cs: "加密套件", cs: "加密套件",
privKey: "私鑰",
pubKey: "公鑰", pubKey: "公鑰",
disableSni: "停用 SNI", disableSni: "停用 SNI",
insecure: "允許不安全連線", insecure: "允許不安全連線",
acme: {
options: "ACME 選項",
dataDir: "數據目錄",
defaultDomain: "默認域名",
disableChallenges: "禁用挑戰",
httpChallenge: "禁用 HTTP 挑戰",
tlsChallenge: "禁用 TLS 挑戰",
altPorts: "替代端口",
altHport: "替代 HTTP 端口",
altTport: "替代 TLS 端口",
caProvider: "CA 提供商",
customCa: "自定義 CA 提供商",
extAcc: "外部賬戶",
dns01: "DNS01 挑戰",
dns01Provider: "DNS01 挑戰提供商"
},
}, },
stats: { stats: {
upload: "上傳", upload: "上傳",
+39 -26
View File
@@ -1,5 +1,5 @@
import { Hysteria, Hysteria2, InTypes, Inbound, Naive, Shadowsocks, TUIC, Trojan, VLESS, VMess } from "@/types/inbounds" import { Hysteria, Hysteria2, InTypes, Inbound, Naive, Shadowsocks, TUIC, Trojan, VLESS, VMess } from "@/types/inbounds"
import { HTTP, WebSocket, QUIC, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport"; import { HTTP, WebSocket, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport";
export interface Link { export interface Link {
type: "local" | "external" | "sub" type: "local" | "external" | "sub"
@@ -13,25 +13,25 @@ function utf8ToBase64(utf8String: string): string {
} }
export namespace LinkUtil { export namespace LinkUtil {
export function linkGenerator(user: string, inbound: Inbound): string { export function linkGenerator(user: string, inbound: Inbound, tlsClient: any = null): string {
const addr = location.hostname const addr = location.hostname
switch(inbound.type){ switch(inbound.type){
case InTypes.Shadowsocks: case InTypes.Shadowsocks:
return shadowsocksLink(user,<Shadowsocks>inbound,addr) return shadowsocksLink(user,<Shadowsocks>inbound, addr)
case InTypes.Naive: case InTypes.Naive:
return naiveLink(user,<Naive>inbound,addr) return naiveLink(user,<Naive>inbound, addr, tlsClient)
case InTypes.Hysteria: case InTypes.Hysteria:
return hysteriaLink(user,<Hysteria>inbound,addr) return hysteriaLink(user,<Hysteria>inbound, addr, tlsClient)
case InTypes.Hysteria2: case InTypes.Hysteria2:
return hysteria2Link(user,<Hysteria2>inbound,addr) return hysteria2Link(user,<Hysteria2>inbound, addr, tlsClient)
case InTypes.TUIC: case InTypes.TUIC:
return tuicLink(user,<TUIC>inbound,addr) return tuicLink(user,<TUIC>inbound, addr, tlsClient)
case InTypes.VLESS: case InTypes.VLESS:
return vlessLink(user,<VLESS>inbound,addr) return vlessLink(user,<VLESS>inbound, addr, tlsClient)
case InTypes.Trojan: case InTypes.Trojan:
return trojanLink(user,<Trojan>inbound,addr) return trojanLink(user,<Trojan>inbound, addr, tlsClient)
case InTypes.VMess: case InTypes.VMess:
return vmessLink(user,<VMess>inbound,addr) return vmessLink(user,<VMess>inbound, addr, tlsClient)
} }
return '' return ''
} }
@@ -40,7 +40,6 @@ export namespace LinkUtil {
const userPass = inbound.users?.find(i => i.name == user)?.password const userPass = inbound.users?.find(i => i.name == user)?.password
const password = [userPass] const password = [userPass]
if (inbound.method.startsWith('2022')) password.push(inbound.password) if (inbound.method.startsWith('2022')) password.push(inbound.password)
const params = { const params = {
tfo: inbound.tcp_fast_open? 1 : null, tfo: inbound.tcp_fast_open? 1 : null,
network: inbound.network?? null network: inbound.network?? null
@@ -56,7 +55,7 @@ export namespace LinkUtil {
return uri.toString() return uri.toString()
} }
function hysteriaLink(user: string, inbound: Hysteria, addr: string): string { function hysteriaLink(user: string, inbound: Hysteria, addr: string, tlsClient: any): string {
const auth = inbound.users.find(i => i.name == user)?.auth_str const auth = inbound.users.find(i => i.name == user)?.auth_str
const params = { const params = {
upmbps: inbound.up_mbps?? null, upmbps: inbound.up_mbps?? null,
@@ -65,7 +64,8 @@ export namespace LinkUtil {
peer: inbound.tls.server_name?? null, peer: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null, alpn: inbound.tls.alpn?.join(',')?? null,
obfsParam: inbound.obfs?? null, obfsParam: inbound.obfs?? null,
fastopen: inbound.tcp_fast_open? 1 : 0 fastopen: inbound.tcp_fast_open? 1 : 0,
insecure: tlsClient?.insecure ? 1 : null
} }
const uri = new URL(`hysteria://${addr}:${inbound.listen_port}`) const uri = new URL(`hysteria://${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){ for (const [key, value] of Object.entries(params)){
@@ -77,7 +77,7 @@ export namespace LinkUtil {
return uri.toString() return uri.toString()
} }
function hysteria2Link(user: string, inbound: Hysteria2, addr: string): string { function hysteria2Link(user: string, inbound: Hysteria2, addr: string, tlsClient: any): string {
const password = inbound.users.find(i => i.name == user)?.password const password = inbound.users.find(i => i.name == user)?.password
const params = { const params = {
upmbps: inbound.up_mbps?? null, upmbps: inbound.up_mbps?? null,
@@ -86,7 +86,8 @@ export namespace LinkUtil {
alpn: inbound.tls.alpn?.join(',')?? null, alpn: inbound.tls.alpn?.join(',')?? null,
obfs: inbound.obfs?.type?? null, obfs: inbound.obfs?.type?? null,
'obfs-password': inbound.obfs?.password?? null, 'obfs-password': inbound.obfs?.password?? null,
fastopen: inbound.tcp_fast_open? 1 : 0 fastopen: inbound.tcp_fast_open? 1 : 0,
insecure: tlsClient?.insecure ? 1 : null
} }
const uri = new URL(`hysteria2://${password}@${addr}:${inbound.listen_port}`) const uri = new URL(`hysteria2://${password}@${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){ for (const [key, value] of Object.entries(params)){
@@ -98,13 +99,14 @@ export namespace LinkUtil {
return uri.toString() return uri.toString()
} }
function naiveLink(user: string, inbound: Naive, addr: string): string { function naiveLink(user: string, inbound: Naive, addr: string, tlsClient: any): string {
const password = inbound.users.find(i => i.username == user)?.password const password = inbound.users.find(i => i.username == user)?.password
const params = { const params = {
padding: 1, padding: 1,
peer: inbound.tls.server_name?? null, peer: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null, alpn: inbound.tls.alpn?.join(',')?? null,
tfo: inbound.tcp_fast_open? 1 : 0 tfo: inbound.tcp_fast_open? 1 : 0,
allowInsecure: tlsClient?.insecure ? 1 : null
} }
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + addr + ":" + inbound.listen_port)}` const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + addr + ":" + inbound.listen_port)}`
const paramsArray = [] const paramsArray = []
@@ -116,12 +118,14 @@ export namespace LinkUtil {
return uri.toString() + "?" + paramsArray.join('&') + "#" + inbound.tag return uri.toString() + "?" + paramsArray.join('&') + "#" + inbound.tag
} }
function tuicLink(user: string, inbound: TUIC, addr: string): string { function tuicLink(user: string, inbound: TUIC, addr: string, tlsClient: any): string {
const u = inbound.users.find(i => i.name == user) const u = inbound.users.find(i => i.name == user)
const params = { const params = {
sni: inbound.tls.server_name?? null, sni: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null, alpn: inbound.tls.alpn?.join(',')?? null,
congestion_control: inbound.congestion_control?? null congestion_control: inbound.congestion_control?? null,
allowInsecure: tlsClient?.insecure ? 1 : null,
disable_sni: tlsClient?.disable_sni ? 1 : null
} }
const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${addr}:${inbound.listen_port}`) const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){ for (const [key, value] of Object.entries(params)){
@@ -166,7 +170,7 @@ export namespace LinkUtil {
return params return params
} }
function vlessLink(user: string, inbound: VLESS, addr: string): string { function vlessLink(user: string, inbound: VLESS, addr: string, tlsClient: any): string {
const u = inbound.users.find(i => i.name == user) const u = inbound.users.find(i => i.name == user)
const transport = <Transport>inbound.transport const transport = <Transport>inbound.transport
@@ -174,10 +178,14 @@ export namespace LinkUtil {
const params = { const params = {
type: transport?.type?? 'tcp', type: transport?.type?? 'tcp',
security: inbound.tls?.enabled? 'tls' : null, security: inbound.tls?.enabled? inbound.tls?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: inbound.tls?.alpn?.join(',')?? null, alpn: inbound.tls?.alpn?.join(',')?? null,
sni: inbound.tls?.server_name?? null, sni: inbound.tls?.server_name?? null,
flow: inbound.tls?.enabled ? u?.flow?? null : 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[0] : null) : null
} }
const uri = new URL(`vless://${u?.uuid}@${addr}:${inbound.listen_port}`) const uri = new URL(`vless://${u?.uuid}@${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries({...params, ...tParams})){ for (const [key, value] of Object.entries({...params, ...tParams})){
@@ -189,7 +197,7 @@ export namespace LinkUtil {
return uri.toString() return uri.toString()
} }
function trojanLink(user: string, inbound: Trojan, addr: string): string { function trojanLink(user: string, inbound: Trojan, addr: string, tlsClient: any): string {
const u = inbound.users.find(i => i.name == user) const u = inbound.users.find(i => i.name == user)
const transport = <Transport>inbound.transport const transport = <Transport>inbound.transport
@@ -197,9 +205,13 @@ export namespace LinkUtil {
const params = { const params = {
type: transport?.type?? 'tcp', type: transport?.type?? 'tcp',
security: inbound.tls?.enabled? 'tls' : null, security: inbound.tls?.enabled? inbound.tls?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: inbound.tls?.alpn?.join(',')?? null, alpn: inbound.tls?.alpn?.join(',')?? null,
sni: inbound.tls?.server_name?? 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[0] : null) : null
} }
const uri = new URL(`trojan://${u?.password}@${addr}:${inbound.listen_port}`) const uri = new URL(`trojan://${u?.password}@${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries({...params, ...tParams})){ for (const [key, value] of Object.entries({...params, ...tParams})){
@@ -211,7 +223,7 @@ export namespace LinkUtil {
return uri.toString() return uri.toString()
} }
function vmessLink(user: string, inbound: VMess, addr: string): string { function vmessLink(user: string, inbound: VMess, addr: string, tlsClient: any): string {
const u = inbound.users.find(i => i.name == user) const u = inbound.users.find(i => i.name == user)
const transport = <Transport>inbound.transport const transport = <Transport>inbound.transport
@@ -230,7 +242,8 @@ export namespace LinkUtil {
port: inbound.listen_port, port: inbound.listen_port,
ps: inbound.tag, ps: inbound.tag,
sni: inbound.tls.server_name?? undefined, sni: inbound.tls.server_name?? undefined,
tls: Object.keys(inbound.tls).length>0? 'tls' : 'none' tls: Object.keys(inbound.tls).length>0? 'tls' : 'none',
allowInsecure: tlsClient?.insecure ? 1 : undefined
} }
return 'vmess://' + utf8ToBase64(JSON.stringify(params, null, 2)) return 'vmess://' + utf8ToBase64(JSON.stringify(params, null, 2))
} }
+5
View File
@@ -39,6 +39,11 @@ const routes = [
name: 'pages.rules', name: 'pages.rules',
component: () => import('@/views/Rules.vue'), component: () => import('@/views/Rules.vue'),
}, },
{
path: '/tls',
name: 'pages.tls',
component: () => import('@/views/Tls.vue'),
},
{ {
path: '/basics', path: '/basics',
name: 'pages.basics', name: 'pages.basics',
+16 -3
View File
@@ -1,7 +1,6 @@
import { FindDiff } from '@/plugins/utils' import { FindDiff } from '@/plugins/utils'
import HttpUtils from '@/plugins/httputil' import HttpUtils from '@/plugins/httputil'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { onMounted } from 'vue'
const Data = defineStore('Data', { const Data = defineStore('Data', {
state: () => ({ state: () => ({
@@ -9,9 +8,10 @@ const Data = defineStore('Data', {
reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[], reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[],
subURI: "", subURI: "",
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]}, onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
oldData: <{config: any, clients: any[]}>{}, oldData: <{config: any, clients: any[], tlsConfigs: any[]}>{},
config: {}, config: {},
clients: [], clients: [],
tlsConfigs: [],
}), }),
actions: { actions: {
async loadData() { async loadData() {
@@ -21,20 +21,23 @@ const Data = defineStore('Data', {
// Set new data // Set new data
const data = JSON.parse(msg.obj) const data = JSON.parse(msg.obj)
if (data.subURI) this.subURI = data.subURI
if (data.config) this.config = data.config if (data.config) this.config = data.config
if (data.clients) this.clients = data.clients if (data.clients) this.clients = data.clients
if (data.subURI) this.subURI = data.subURI if (data.tls) this.tlsConfigs = data.tls
this.onlines = data.onlines this.onlines = data.onlines
// To avoid ref copy // To avoid ref copy
if (data.config) this.oldData.config = { ...JSON.parse(msg.obj).config } if (data.config) this.oldData.config = { ...JSON.parse(msg.obj).config }
if (data.clients) this.oldData.clients = [ ...JSON.parse(msg.obj).clients ] if (data.clients) this.oldData.clients = [ ...JSON.parse(msg.obj).clients ]
if (data.tls) this.oldData.tlsConfigs = [ ...JSON.parse(msg.obj).tls ]
} }
}, },
async pushData() { async pushData() {
const diff = { const diff = {
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config)), config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config)),
clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)), clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)),
tls: JSON.stringify(FindDiff.Clients(this.tlsConfigs,this.oldData.tlsConfigs)),
} }
const msg = await HttpUtils.post('api/save',diff) const msg = await HttpUtils.post('api/save',diff)
if(msg.success) { if(msg.success) {
@@ -45,6 +48,7 @@ const Data = defineStore('Data', {
const diff = { const diff = {
config: JSON.stringify([{key: "inbounds", action: "del", index: index, obj: null}]), config: JSON.stringify([{key: "inbounds", action: "del", index: index, obj: null}]),
clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)), clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)),
tls: JSON.stringify(FindDiff.Clients(this.tlsConfigs,this.oldData.tlsConfigs)),
} }
const msg = await HttpUtils.post('api/save',diff) const msg = await HttpUtils.post('api/save',diff)
if(msg.success) { if(msg.success) {
@@ -69,6 +73,15 @@ const Data = defineStore('Data', {
if(msg.success) { if(msg.success) {
this.loadData() 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()
}
} }
}, },
}) })
+46
View File
@@ -1,3 +1,5 @@
import { Dial } from "./dial"
export interface iTls { export interface iTls {
enabled?: boolean enabled?: boolean
server_name?: string server_name?: string
@@ -9,6 +11,50 @@ export interface iTls {
certificate_path?: string certificate_path?: string
key?: string[] key?: string[]
key_path?: string key_path?: string
acme?: acme
ech?: ech
reality?: reality
}
export interface acme {
domain: string[]
data_directory?: string
default_server_name?: string
email?: string
provider?: string
disable_http_challenge?: boolean
disable_tls_alpn_challenge?: boolean
alternative_http_port?: number
alternative_tls_port?: number
external_account?: {
key_id: string
mac_key: string
}
dns01_challenge?: {
provider: string
[key: string]: string
}
}
export interface ech {
enabled: boolean
pq_signature_schemes_enabled?: boolean
dynamic_record_sizing_disabled?: boolean
key?: string[]
key_path?: string
}
interface realityHanshake extends Dial {
server: string
server_port: number
}
export interface reality {
enabled: boolean
handshake: realityHanshake
private_key: string
short_id: string[]
max_time_difference?: string
} }
export const defaultInTls: iTls = { export const defaultInTls: iTls = {
+2 -1
View File
@@ -261,7 +261,8 @@ const updateLinks = (c:Client):string => {
const clientInbounds = <Inbound[]>inbounds.value.filter(i => c.inbounds.split(',').includes(i.tag)) const clientInbounds = <Inbound[]>inbounds.value.filter(i => c.inbounds.split(',').includes(i.tag))
const newLinks = <Link[]>[] const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{ clientInbounds.forEach(i =>{
const uri = LinkUtil.linkGenerator(c.name,i) const tlsConfig = <any>Data().tlsConfigs?.findLast((t:any) => t.inbounds.includes(i.tag))
const uri = LinkUtil.linkGenerator(c.name,i,tlsConfig?.client)
if (uri.length>0){ if (uri.length>0){
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri }) newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
} }
+30 -3
View File
@@ -7,6 +7,7 @@
:data="modal.data" :data="modal.data"
:inTags="inTags" :inTags="inTags"
:outTags="outTags" :outTags="outTags"
:tlsConfigs="tlsConfigs"
@close="closeModal" @close="closeModal"
@save="saveModal" @save="saveModal"
/> />
@@ -119,6 +120,10 @@ const inbounds = computed((): Inbound[] => {
return <Inbound[]> appConfig.value.inbounds return <Inbound[]> appConfig.value.inbounds
}) })
const tlsConfigs = computed((): any[] => {
return <any[]> Data().tlsConfigs
})
const inTags = computed((): string[] => { const inTags = computed((): string[] => {
return inbounds.value?.map(i => i.tag) return inbounds.value?.map(i => i.tag)
}) })
@@ -157,7 +162,7 @@ const showModal = (id: number) => {
const closeModal = () => { const closeModal = () => {
modal.value.visible = false modal.value.visible = false
} }
const saveModal = (data:Inbound, stats: boolean) => { const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
// Check duplicate tag // Check duplicate tag
const oldTag = modal.value.id != -1 ? inbounds.value[modal.value.id].tag : null const oldTag = modal.value.id != -1 ? inbounds.value[modal.value.id].tag : null
if (data.tag != oldTag && inTags.value.includes(data.tag)) { if (data.tag != oldTag && inTags.value.includes(data.tag)) {
@@ -165,16 +170,30 @@ const saveModal = (data:Inbound, stats: boolean) => {
sb.showMessage(i18n.global.t('error.dplData') + ': ' + i18n.global.t('objects.tag') ,'error', 5000) sb.showMessage(i18n.global.t('error.dplData') + ': ' + i18n.global.t('objects.tag') ,'error', 5000)
return return
} }
// New or Edit // New or Edit
if (modal.value.id == -1) { if (modal.value.id == -1) {
inbounds.value.push(data) inbounds.value.push(data)
if (stats && data.tag.length>0) { if (stats && data.tag.length>0) {
v2rayStats.value.inbounds.push(data.tag) v2rayStats.value.inbounds.push(data.tag)
} }
// Update tls preset
if (tls_id>0) {
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
}
} else { } else {
const oldTag = inbounds.value[modal.value.id].tag const oldTag = inbounds.value[modal.value.id].tag
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == data.tag) // Find if new tag exists 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 (tls_id>0) {
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
}
if (oldTag != data.tag) { if (oldTag != data.tag) {
v2rayStats.value.inbounds = v2rayStats.value.inbounds.filter(item => item != oldTag) v2rayStats.value.inbounds = v2rayStats.value.inbounds.filter(item => item != oldTag)
changeClientInboundsTag(oldTag,data.tag) changeClientInboundsTag(oldTag,data.tag)
@@ -206,7 +225,8 @@ const updateLinks = (i: InboundWithUser) => {
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.split(',').includes(inb.tag)) const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.split(',').includes(inb.tag))
const newLinks = <Link[]>[] const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{ clientInbounds.forEach(i =>{
const uri = LinkUtil.linkGenerator(client.name,i) const tlsClient = tlsConfigs?.value.findLast((t:any) => t.inbounds.includes(i.tag))?.client?? null
const uri = LinkUtil.linkGenerator(client.name,i, tlsClient)
if (uri.length>0){ if (uri.length>0){
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri }) newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
} }
@@ -224,7 +244,7 @@ const delInbound = (index: number) => {
inbounds.value.splice(index,1) inbounds.value.splice(index,1)
const tag = inb.tag const tag = inb.tag
if (Object.hasOwn(inb,'users')){ if (Object.hasOwn(inb,'users')) {
const inbU = <InboundWithUser>inb const inbU = <InboundWithUser>inb
if (inbU.users && inbU.users.length>0){ if (inbU.users && inbU.users.length>0){
inbU.users.forEach((u:any) => { inbU.users.forEach((u:any) => {
@@ -237,6 +257,13 @@ const delInbound = (index: number) => {
} }
} }
// 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 // Delete stats if exists and will be orphaned
const tagCounts = inbounds.value.filter(i => i.tag == inb.tag).length const tagCounts = inbounds.value.filter(i => i.tag == inb.tag).length
+160
View File
@@ -0,0 +1,160 @@
<template>
<TlsVue
v-model="modal.visible"
:visible="modal.visible"
:index="modal.index"
: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-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.name">
<v-card-subtitle style="margin-top: -20px;">
{{ item.server?.server_name?.length>0 ? item.server.server_name : "-" }}
</v-card-subtitle>
<v-card-text>
<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 }}
</v-col>
</v-row>
<v-row>
<v-col>ACME</v-col>
<v-col dir="ltr">
{{ $t(item.server?.acme == undefined ? 'no' : 'yes') }}
</v-col>
</v-row>
<v-row>
<v-col>ECH</v-col>
<v-col dir="ltr">
{{ $t(item.server?.ech == undefined ? 'no' : 'yes') }}
</v-col>
</v-row>
<v-row>
<v-col>Reality</v-col>
<v-col dir="ltr">
{{ $t(item.server?.reality == undefined ? 'no' : 'yes') }}
</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(index)">
<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-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="delTls(index)">{{ $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-card-actions>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts" setup>
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 { Client } from '@/types/clients';
import { Link, LinkUtil } from '@/plugins/link';
const tlsConfigs = computed((): any[] => {
return Data().tlsConfigs
})
const inbounds = computed((): any[] => {
return <any[]>(<Config>Data().config)?.inbounds
})
const clients = computed((): any[] => {
return <Client[]>Data().clients
})
const modal = ref({
visible: false,
index: -1,
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])
modal.value.visible = true
}
const closeModal = () => {
modal.value.visible = false
}
const saveModal = (data: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
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 && i.users.length>0){
i.users.forEach((u:any) => {
const client = clients.value.find(c => u.username? c.name == u.username : c.name == u.name)
if (client){
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.split(',').includes(inb.tag))
const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{
const uri = LinkUtil.linkGenerator(client.name,i,tlsClient)
if (uri.length>0){
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
}
})
let links = client.links && client.links.length>0? <Link[]>JSON.parse(client.links) : <Link[]>[]
links = [...newLinks, ...links.filter(l => l.type != 'local')]
client.links = JSON.stringify(links)
}
})
}
}
</script>