optimize security in front codes

This commit is contained in:
Alireza Ahmadi
2023-11-25 16:11:26 +01:00
parent 54197993db
commit 7410b80e7a
11 changed files with 243 additions and 266 deletions

View File

@@ -8,13 +8,6 @@ const Protocols = {
HTTP: 'http',
};
const VmessMethods = {
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
AUTO: 'auto',
NONE: 'none',
};
const SSMethods = {
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
@@ -86,7 +79,6 @@ const SNIFFING_OPTION = {
};
Object.freeze(Protocols);
Object.freeze(VmessMethods);
Object.freeze(SSMethods);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION);
@@ -413,7 +405,7 @@ class HttpStreamSettings extends XrayCommonClass {
}
class QuicStreamSettings extends XrayCommonClass {
constructor(security=VmessMethods.NONE,
constructor(security='none',
key='', type='none') {
super();
this.security = security;
@@ -856,30 +848,6 @@ class Inbound extends XrayCommonClass {
}
}
get tls() {
return this.stream.security === 'tls';
}
set tls(isTls) {
if (isTls) {
this.stream.security = 'tls';
} else {
this.stream.security = 'none';
}
}
get reality() {
return this.stream.security === 'reality';
}
set reality(isReality) {
if (isReality) {
this.stream.security = 'reality';
} else {
this.stream.security = 'none';
}
}
get network() {
return this.stream.network;
}
@@ -1005,85 +973,29 @@ class Inbound extends XrayCommonClass {
}
canEnableTls() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
break;
default:
return false;
}
switch (this.network) {
case "tcp":
case "ws":
case "http":
case "quic":
case "grpc":
return true;
default:
return false;
}
if(![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.network);
}
//this is used for xtls-rprx-vision
canEnableTlsFlow() {
if ((this.stream.security != 'none') && (this.network === "tcp")) {
switch (this.protocol) {
case Protocols.VLESS:
return true;
default:
return false;
}
return this.protocol === Protocols.VLESS;
}
return false;
}
canSetTls() {
return this.canEnableTls();
}
canEnableReality() {
switch (this.protocol) {
case Protocols.VLESS:
case Protocols.TROJAN:
break;
default:
return false;
}
switch (this.network) {
case "tcp":
case "http":
case "grpc":
return true;
default:
return false;
}
if(![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return ["tcp", "http", "grpc"].includes(this.network);
}
canEnableStream() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
return false;
}
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
}
canSniffing() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
return false;
}
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
}
reset() {
@@ -1485,7 +1397,7 @@ class Inbound extends XrayCommonClass {
toJson() {
let streamSettings;
if (this.canEnableStream() || this.protocol === Protocols.TROJAN) {
if (this.canEnableStream()) {
streamSettings = this.stream.toJson();
}
return {

View File

@@ -38,7 +38,7 @@
address = this.dbInbound.address;
this.subId = '';
this.qrcodes = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
if (this.inbound.stream.isTls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
remarkText = [remark, domain.remark].filter(Boolean).join('-');
this.qrcodes.push({

View File

@@ -19,7 +19,7 @@
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && inbound.tls">
<template v-if="inbound.isTcp && inbound.stream.isTls">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>

View File

@@ -21,7 +21,7 @@
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && inbound.tls">
<template v-if="inbound.isTcp && inbound.stream.isTls">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>

View File

@@ -1,192 +1,195 @@
{{define "form/tlsSettings"}}
<!-- tls enable -->
<a-form v-if="inbound.canSetTls()" layout="inline">
<a-form v-if="inbound.canEnableTls()" layout="inline">
<a-divider style="margin:0;"></a-divider>
<a-form-item label="TLS">
<a-switch v-model="inbound.tls">
</a-switch>
</a-form-item>
<a-form-item label="Reality" v-if="inbound.canEnableReality()">
<a-switch v-model="inbound.reality"></a-switch>
</a-form-item>
</a-form>
<table width="100%" class="ant-table-tbody">
<tr>
<td width="30%">{{ i18n "security" }}</td>
<td>
<a-form-item>
<a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
</a-radio-group>
</a-form-item>
</td>
</tr>
<!-- tls settings -->
<a-form v-if="inbound.tls" layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>SNI</td>
<td>
<a-form-item placeholder="Server Name Indication" v-if="inbound.tls">
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>CipherSuites</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
<template v-if="inbound.stream.isTls">
<tr>
<td>SNI</td>
<td>
<a-form-item placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>CipherSuites</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Min/Max Version</td>
<td>
<a-form-item>
<a-input-group compact>
<a-select style="width: 125px" v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Min/Max Version</td>
<td>
<a-form-item>
<a-input-group compact>
<a-select style="width: 125px" v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
<a-select style="width: 125px" v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
<a-select style="width: 125px" v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr style="line-height: 40px;">
<td>Multi Domain</td>
<td>
<a-switch v-model="multiDomain"></a-switch>
<a-button v-if="multiDomain" style="margin-left: 10px" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button>
</td>
</tr>
<tr v-if="multiDomain" style="line-height: 40px;">
<td colspan="2" width="100%">
<a-input-group style="margin-top:5px;" compact v-for="(row, index) in inbound.stream.tls.settings.domains">
<a-input style="width: 50%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="row.domain" placeholder='{{ i18n "host" }}'>
<a-button slot="addonAfter" size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
</a-input>
</a-input-group>
</td>
</tr>
<tr v-else>
<td>{{ i18n "domainName" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>ALPN</td>
<td>
<a-form-item>
<a-select
mode="multiple"
style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Allow insecure</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>Reject Unknown SNI</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
</td>
</tr>
<template v-for="cert,index in inbound.stream.tls.certs">
<tr>
<td>{{ i18n "certificate" }}</td>
<td>
<a-form-item>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
</td>
</tr>
<tr style="line-height: 40px;">
<td>Multi Domain</td>
<td>
<a-switch v-model="multiDomain"></a-switch>
<a-button v-if="multiDomain" style="margin-left: 10px" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button>
</td>
</tr>
<tr v-if="multiDomain" style="line-height: 40px;">
<td colspan="2" width="100%">
<a-input-group style="margin-top:5px;" compact v-for="(row, index) in inbound.stream.tls.settings.domains">
<a-input style="width: 50%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="row.domain" placeholder='{{ i18n "host" }}'>
<a-button slot="addonAfter" size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
</a-input>
</a-input-group>
</td>
</tr>
<tr v-else>
<td>{{ i18n "domainName" }}</td>
<template v-if="cert.useFile">
<tr>
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
<a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>ALPN</td>
<td>{{ i18n "pages.inbounds.keyPath" }}</td>
<td>
<a-form-item>
<a-select
mode="multiple"
style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
<a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Allow insecure</td>
<td></td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</td>
</tr>
<tr>
<td>Reject Unknown SNI</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
</td>
</tr>
<template v-for="cert,index in inbound.stream.tls.certs">
<tr>
<td colspan="2" width="100%">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
</td>
</tr>
<template v-if="cert.useFile">
<tr>
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td></td>
<td>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</td>
</tr>
</template>
<template v-else>
<tr>
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
</a-form-item>
</td>
</tr>
</template>
<tr>
<td>ocspStapling</td>
<td>
<a-form-item>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
</template>
</table>
</a-form>
<template v-else>
<tr>
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
</a-form-item>
</td>
</tr>
</template>
<tr>
<td>ocspStapling</td>
<td>
<a-form-item>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- reality settings -->
<a-form v-if="inbound.reality" layout="inline">
<table width="100%" class="ant-table-tbody">
<template v-if="inbound.stream.isReality">
<tr>
<td>{{ i18n "domainName" }}</td>
<td>
@@ -278,6 +281,7 @@
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get new cert</a-button>
</td>
</tr>
</template>
</table>
</a-form>
{{end}}

View File

@@ -51,11 +51,11 @@
</table>
</td></tr>
<tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls">
<td v-if="inbound.stream.isTls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.reality">
<td v-else-if="inbound.stream.isReality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
@@ -268,7 +268,7 @@
remark = [this.dbInbound.remark, ( this.clientSettings ? this.clientSettings.email : '')].filter(Boolean).join('-');
address = this.dbInbound.address;
this.links = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
if (this.inbound.stream.isTls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
remarkText = [remark, domain.remark].filter(Boolean).join('-');
this.links.push({

View File

@@ -92,11 +92,11 @@
},
methods: {
streamNetworkChange() {
if (!inModal.inbound.canSetTls()) {
if (!inModal.inbound.canEnableTls()) {
this.inModal.inbound.stream.security = 'none';
}
if (!inModal.inbound.canEnableReality()) {
this.inModal.inbound.reality = false;
this.inModal.inbound.stream.isReality = false;
}
if (this.inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
this.inModal.inbound.settings.vlesses.forEach(client => {

View File

@@ -52,6 +52,7 @@
"remained" = "Remained"
"secAlertTitle" = "Security Alert"
"secAlertSsl" = "This connection is not secure; Please refrain from entering sensitive information until TLS is activated for data protection"
"security" = "Security"
[menu]
"dashboard" = "System Status"
@@ -362,6 +363,20 @@
"edit" = "Edit Rule"
"useComma" = "Comma separated items"
[pages.xray.outbound]
"addOutbound" = "Add outbound"
"addReverse" = "Add reverse"
"editOutbound" = "Edit outbound"
"editReverse" = "Edit reverse"
"tag" = "Tag"
"address" = "Address"
"reverse" = "Reverse"
"domain" = "Domain"
"type" = "Type"
"bridge" = "Bridge"
"portal" = "Portal"
"intercon" = "Interconnection"
[tgbot]
"noResult" = "❗ No result!"
"wentWrong" = "❌ Something went wrong!"

View File

@@ -52,6 +52,7 @@
"remained" = "باقیمانده"
"secAlertTitle" = "هشدار امنیتی"
"secAlertSsl" = "این اتصال امن نیست؛ لطفا تا زمانی که تی‌ال‌اس برای حفاظت از داده ها فعال نشده است از وارد کردن اطلاعات حساس خودداری کنید"
"security" = "امنیت"
[menu]
"dashboard" = "وضعیت سیستم"
@@ -361,6 +362,21 @@
"edit" = "ویرایش قانون"
"useComma" = "موارد جدا شده با کاما"
[pages.xray.outbound]
"addOutbound" = "افزودن خروجی"
"addReverse" = "افزودن معکوس"
"editOutbound" = "ویرایش خروجی"
"editReverse" = "ویرایش معکوس"
"tag" = "برچسب"
"address" = "آدرس"
"reverse" = "معکوس"
"domain" = "دامنه"
"type" = "نوع"
"bridge" = "پل"
"portal" = "پرتال"
"intercon" = "اتصال میانی"
[tgbot]
"noResult" = "❗ نتیجه‌ای یافت نشد!"
"wentWrong" = "❌ مشکلی رخ داده است!"

View File

@@ -52,6 +52,7 @@
"remained" = "Осталось"
"secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
"security" = "Безопасность"
[menu]
"dashboard" = "Статус системы"
@@ -362,6 +363,20 @@
"edit" = "Редактировать правило"
"useComma" = "Элементы, разделенные запятыми"
[pages.xray.outbound]
"addOutbound" = "Добавить исходящий"
"addReverse" = "Добавить реверс"
"editOutbound" = "Изменить исходящий"
"editReverse" = "Редактировать реверс"
"tag" = "Тег"
"address" = "Адрес"
"reverse" = "Обратный"
"domain" = "Домен"
"type" = "Тип"
"bridge" = "Мост"
"portal" = "Портал"
"intercon" = "Соединение"
[tgbot]
"noResult" = "❗ Нет результатов!"
"wentWrong" = "❌ Что-то пошло не так!"

View File

@@ -52,6 +52,7 @@
"remained" = "仍然存在"
"secAlertTitle" = "安全警报"
"secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
"security" = "安全"
[menu]
"dashboard" = "系统状态"
@@ -362,6 +363,20 @@
"edit" = "编辑规则"
"useComma" = "逗号分隔的项目"
[pages.xray.outbound]
"addOutbound" = "添加出站"
"addReverse" = "添加反向"
"editOutbound" = "编辑出站"
"editReverse" = "编辑反向"
"tag" = "标签"
"address" = "地址"
"rreverse" = "反转"
"domain" = "域名"
"type" = "类型"
"bridge" = "桥"
"portal" = "门户"
"intercon" = "互连"
[tgbot]
"noResult" = "❗ 没有结果!"
"wentWrong" = "❌ 出了点问题!"