mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 07:37:15 +00:00
fix(sub): prevent default profile page URL disclosure (#6538)
* fix(sub): prevent default profile page URL disclosure Add explicit none, builtin, and custom profile page modes. Preserve existing custom URLs and warn before exposing the built-in page. Cover mode selection, legacy settings, and subscription response headers. * fix(subscription): add profile page link options and upgrade notes
This commit is contained in:
@@ -94,6 +94,7 @@ type AllSetting struct {
|
||||
SubClashUserAgentRegex string `json:"subClashUserAgentRegex" form:"subClashUserAgentRegex"`
|
||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||
SubSupportUrl string `json:"subSupportUrl" form:"subSupportUrl"`
|
||||
SubProfileMode string `json:"subProfileMode" form:"subProfileMode"`
|
||||
SubProfileUrl string `json:"subProfileUrl" form:"subProfileUrl"`
|
||||
SubAnnounce string `json:"subAnnounce" form:"subAnnounce"`
|
||||
SubEnableRouting bool `json:"subEnableRouting" form:"subEnableRouting"`
|
||||
|
||||
@@ -44,6 +44,13 @@ const (
|
||||
maxRegexLength = 2048
|
||||
)
|
||||
|
||||
// Built-in profile links expose the subscription URL and require an explicit opt-in.
|
||||
const (
|
||||
SubProfileModeNone = "none"
|
||||
SubProfileModeBuiltin = "builtin"
|
||||
SubProfileModeCustom = "custom"
|
||||
)
|
||||
|
||||
var defaultValueMap = map[string]string{
|
||||
"xrayTemplateConfig": xrayTemplateConfig,
|
||||
"webListen": "",
|
||||
@@ -101,6 +108,7 @@ var defaultValueMap = map[string]string{
|
||||
"subClashUserAgentRegex": "",
|
||||
"subTitle": "",
|
||||
"subSupportUrl": "",
|
||||
"subProfileMode": SubProfileModeNone,
|
||||
"subProfileUrl": "",
|
||||
"subAnnounce": "",
|
||||
"subEnableRouting": "false",
|
||||
@@ -298,6 +306,11 @@ func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// A missing mode must still preserve URLs configured before modes existed.
|
||||
if !keyMap["subProfileMode"] {
|
||||
allSetting.SubProfileMode = ""
|
||||
}
|
||||
allSetting.SubProfileMode = effectiveSubProfileMode(allSetting.SubProfileMode, allSetting.SubProfileUrl)
|
||||
return allSetting, nil
|
||||
}
|
||||
|
||||
@@ -850,6 +863,34 @@ func (s *SettingService) GetSubProfileUrl() (string, error) {
|
||||
return common.EnsureURLScheme(value), err
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubProfileMode() (string, error) {
|
||||
setting, err := s.getSetting("subProfileMode")
|
||||
if err != nil && !database.IsNotFound(err) {
|
||||
return SubProfileModeNone, err
|
||||
}
|
||||
if err == nil && setting.Value != "" {
|
||||
return effectiveSubProfileMode(setting.Value, ""), nil
|
||||
}
|
||||
profileURL, err := s.getString("subProfileUrl")
|
||||
if err != nil {
|
||||
return SubProfileModeNone, err
|
||||
}
|
||||
return effectiveSubProfileMode("", profileURL), nil
|
||||
}
|
||||
|
||||
func effectiveSubProfileMode(mode, profileURL string) string {
|
||||
switch mode {
|
||||
case SubProfileModeNone, SubProfileModeBuiltin, SubProfileModeCustom:
|
||||
return mode
|
||||
case "":
|
||||
// Older settings have no mode; only an existing custom URL opts them in.
|
||||
if strings.TrimSpace(profileURL) != "" {
|
||||
return SubProfileModeCustom
|
||||
}
|
||||
}
|
||||
return SubProfileModeNone
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubAnnounce() (string, error) {
|
||||
return s.getString("subAnnounce")
|
||||
}
|
||||
@@ -1448,6 +1489,12 @@ type SecretClears struct {
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting, clears SecretClears) error {
|
||||
switch allSetting.SubProfileMode {
|
||||
case "", SubProfileModeNone, SubProfileModeBuiltin, SubProfileModeCustom:
|
||||
allSetting.SubProfileMode = effectiveSubProfileMode(allSetting.SubProfileMode, allSetting.SubProfileUrl)
|
||||
default:
|
||||
return errors.New("subscription profile mode must be none, builtin, or custom")
|
||||
}
|
||||
if err := s.preserveRedactedSecrets(allSetting, clears); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
func TestSubProfileModeReadsLegacyAndExplicitSettings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
storedMode string
|
||||
storedURL string
|
||||
modeExists bool
|
||||
want string
|
||||
}{
|
||||
{name: "fresh installation", want: "none"},
|
||||
{name: "legacy URL", storedURL: " https://profile.example/account ", want: "custom"},
|
||||
{name: "legacy blank URL", storedURL: " \t ", want: "none"},
|
||||
{name: "legacy empty mode with URL", modeExists: true, storedURL: "https://profile.example/account", want: "custom"},
|
||||
{name: "legacy empty mode without URL", modeExists: true, want: "none"},
|
||||
{name: "explicit none preserves saved URL", modeExists: true, storedMode: "none", storedURL: "https://profile.example/account", want: "none"},
|
||||
{name: "explicit builtin", modeExists: true, storedMode: "builtin", storedURL: "https://profile.example/account", want: "builtin"},
|
||||
{name: "explicit custom", modeExists: true, storedMode: "custom", storedURL: "https://profile.example/account", want: "custom"},
|
||||
{name: "custom with empty URL", modeExists: true, storedMode: "custom", want: "custom"},
|
||||
{name: "invalid stored mode fails closed", modeExists: true, storedMode: "automatic", storedURL: "https://profile.example/account", want: "none"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setupSettingTestDB(t)
|
||||
s := &SettingService{}
|
||||
if tt.modeExists {
|
||||
if err := s.saveSetting("subProfileMode", tt.storedMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := s.saveSetting("subProfileUrl", tt.storedURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSubProfileSettings(t, s, tt.want, tt.storedURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubProfileModeUpdatesPreserveURLAndLegacyPayloads(t *testing.T) {
|
||||
setupSettingTestDB(t)
|
||||
s := &SettingService{}
|
||||
if got := s.GetFactoryDefaults()["subProfileMode"]; got != "none" {
|
||||
t.Errorf("factory profile mode = %q, want none", got)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
mode string
|
||||
url string
|
||||
want string
|
||||
}{
|
||||
{name: "custom", mode: "custom", url: "https://profile.example/account", want: "custom"},
|
||||
{name: "none retains custom URL", mode: "none", url: "https://profile.example/account", want: "none"},
|
||||
{name: "builtin retains custom URL", mode: "builtin", url: "https://profile.example/account", want: "builtin"},
|
||||
{name: "custom restores saved URL", mode: "custom", url: "https://profile.example/account", want: "custom"},
|
||||
{name: "legacy URL submission", url: "https://legacy.example/account", want: "custom"},
|
||||
{name: "legacy empty URL submission", want: "none"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
settings, err := s.GetAllSetting()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := json.Marshal(map[string]string{"subProfileMode": tt.mode, "subProfileUrl": tt.url})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(payload, settings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSubProfileSettings(t, s, tt.want, tt.url)
|
||||
stored, err := s.getSetting("subProfileMode")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.Value != tt.want {
|
||||
t.Fatalf("persisted mode = %q, want %q", stored.Value, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubProfileModeRejectsInvalidUpdateBeforeWrites(t *testing.T) {
|
||||
setupSettingTestDB(t)
|
||||
s := &SettingService{}
|
||||
if err := s.saveSetting("subTitle", "Original title"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var before []model.Setting
|
||||
if err := database.GetDB().Order("id").Find(&before).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings, err := s.GetAllSetting()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(`{"subProfileMode":"automatic","subTitle":"Changed title","subProfileUrl":"https://profile.example/account"}`), settings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = s.UpdateAllSetting(settings, SecretClears{})
|
||||
if err == nil || err.Error() != "subscription profile mode must be none, builtin, or custom" {
|
||||
t.Errorf("UpdateAllSetting error = %v, want invalid profile mode error", err)
|
||||
}
|
||||
var after []model.Setting
|
||||
if err := database.GetDB().Order("id").Find(&after).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
t.Fatal("invalid profile mode update modified stored settings")
|
||||
}
|
||||
}
|
||||
|
||||
func assertSubProfileSettings(t *testing.T, s *SettingService, wantMode, wantURL string) {
|
||||
t.Helper()
|
||||
if mode, err := s.GetSubProfileMode(); err != nil || mode != wantMode {
|
||||
t.Fatalf("GetSubProfileMode = %q, %v; want %q, nil", mode, err, wantMode)
|
||||
}
|
||||
settings, err := s.GetAllSetting()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var profile struct {
|
||||
Mode string `json:"subProfileMode"`
|
||||
URL string `json:"subProfileUrl"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &profile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if profile.Mode != wantMode || profile.URL != wantURL {
|
||||
t.Fatalf("profile settings = (%q, %q), want (%q, %q)", profile.Mode, profile.URL, wantMode, wantURL)
|
||||
}
|
||||
}
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "العنوان اللي هيظهر في عميل VPN. يدعم رموز هوية العميل: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "رابط الدعم",
|
||||
"subSupportUrlDesc": "رابط الدعم الفني المعروض في عميل VPN. يدعم رموز هوية العميل: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "صفحة الملف الشخصي",
|
||||
"subProfileModeDesc": "اختر رابط الموقع الإلكتروني الذي يظهر في عميل VPN.",
|
||||
"subProfileModeNone": "بدون رابط",
|
||||
"subProfileModeBuiltin": "صفحة الاشتراك المدمجة",
|
||||
"subProfileModeCustom": "موقع إلكتروني مخصص",
|
||||
"subProfileBuiltinWarning": "تكشف هذه الصفحة روابط الاشتراك وإعدادات العقد، بما في ذلك اشتراكات Happ المشفرة.",
|
||||
"subProfileUrl": "رابط الملف الشخصي",
|
||||
"subProfileUrlDesc": "رابط لموقعك الإلكتروني يظهر في عميل VPN. يدعم رموز هوية العميل: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "رابط لموقعك الإلكتروني يظهر في عميل VPN. يدعم رموز هوية العميل: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. اتركه فارغًا لعدم عرض رابط الموقع في عميل VPN.",
|
||||
"subAnnounce": "إعلان",
|
||||
"subAnnounceDesc": "نص الإعلان المعروض في عميل VPN. يدعم رموز هوية العميل: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "مجلد قالب الاشتراك",
|
||||
|
||||
@@ -1322,8 +1322,14 @@
|
||||
"subTitleDesc": "Title shown in VPN client. Supports client identity tokens: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "Support URL",
|
||||
"subSupportUrlDesc": "Technical support link shown in the VPN client. Supports client identity tokens: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "Profile page",
|
||||
"subProfileModeDesc": "Choose which website link is shown in the VPN client.",
|
||||
"subProfileModeNone": "No link",
|
||||
"subProfileModeBuiltin": "Built-in subscription page",
|
||||
"subProfileModeCustom": "Custom website",
|
||||
"subProfileBuiltinWarning": "This page exposes subscription URLs and node configurations, including for Happ encrypted subscriptions.",
|
||||
"subProfileUrl": "Profile URL",
|
||||
"subProfileUrlDesc": "A link to your website displayed in the VPN client. Supports client identity tokens: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "A link to your website displayed in the VPN client. Supports client identity tokens: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. Leave empty to omit the website link.",
|
||||
"subAnnounce": "Announce",
|
||||
"subAnnounceDesc": "The announcement text displayed in the VPN client. Supports client identity tokens: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "Sub Theme Directory",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "Título mostrado en el cliente VPN. Admite tokens de identidad del cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "URL de soporte",
|
||||
"subSupportUrlDesc": "Enlace de soporte técnico mostrado en el cliente VPN. Admite tokens de identidad del cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "Página de perfil",
|
||||
"subProfileModeDesc": "Elige qué enlace al sitio web se muestra en el cliente VPN.",
|
||||
"subProfileModeNone": "Sin enlace",
|
||||
"subProfileModeBuiltin": "Página de suscripción integrada",
|
||||
"subProfileModeCustom": "Sitio web personalizado",
|
||||
"subProfileBuiltinWarning": "Esta página expone las URL de suscripción y las configuraciones de los nodos, incluso para las suscripciones cifradas de Happ.",
|
||||
"subProfileUrl": "URL del perfil",
|
||||
"subProfileUrlDesc": "Un enlace a tu sitio web mostrado en el cliente VPN. Admite tokens de identidad del cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "Un enlace a tu sitio web mostrado en el cliente VPN. Admite tokens de identidad del cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. Déjalo vacío para omitir el enlace al sitio web en el cliente VPN.",
|
||||
"subAnnounce": "Anuncio",
|
||||
"subAnnounceDesc": "El texto del anuncio mostrado en el cliente VPN. Admite tokens de identidad del cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "Directorio del tema de suscripción",
|
||||
|
||||
@@ -1204,8 +1204,14 @@
|
||||
"subTitleDesc": "عنوان نمایش داده شده در کلاینت VPN. از توکنهای هویت کلاینت پشتیبانی میکند: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "آدرس پشتیبانی",
|
||||
"subSupportUrlDesc": "لینک پشتیبانی فنی که در کلاینت VPN نمایش داده میشود. از توکنهای هویت کلاینت پشتیبانی میکند: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "صفحه پروفایل",
|
||||
"subProfileModeDesc": "انتخاب کنید کدام لینک وبسایت در کلاینت VPN نمایش داده شود.",
|
||||
"subProfileModeNone": "بدون لینک",
|
||||
"subProfileModeBuiltin": "صفحه اشتراک داخلی",
|
||||
"subProfileModeCustom": "وبسایت سفارشی",
|
||||
"subProfileBuiltinWarning": "این صفحه آدرسهای اشتراک و پیکربندی گرهها را آشکار میکند، حتی برای اشتراکهای رمزگذاریشده Happ.",
|
||||
"subProfileUrl": "آدرس پروفایل",
|
||||
"subProfileUrlDesc": "لینک وبسایت شما که در کلاینت VPN نمایش داده میشود. از توکنهای هویت کلاینت پشتیبانی میکند: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "لینک وبسایت شما که در کلاینت VPN نمایش داده میشود. از توکنهای هویت کلاینت پشتیبانی میکند: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. برای عدم نمایش لینک وبسایت در کلاینت VPN، این فیلد را خالی بگذارید.",
|
||||
"subAnnounce": "اعلان",
|
||||
"subAnnounceDesc": "متن اعلانی که در کلاینت VPN نمایش داده میشود. از توکنهای هویت کلاینت پشتیبانی میکند: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "پوشه قالب صفحه اشتراک",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "Judul yang ditampilkan di klien VPN. Mendukung token identitas klien: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "URL Dukungan",
|
||||
"subSupportUrlDesc": "Tautan dukungan teknis yang ditampilkan di klien VPN. Mendukung token identitas klien: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "Halaman profil",
|
||||
"subProfileModeDesc": "Pilih tautan situs web yang ditampilkan di klien VPN.",
|
||||
"subProfileModeNone": "Tanpa tautan",
|
||||
"subProfileModeBuiltin": "Halaman langganan bawaan",
|
||||
"subProfileModeCustom": "Situs web kustom",
|
||||
"subProfileBuiltinWarning": "Halaman ini menampilkan URL langganan dan konfigurasi node, termasuk untuk langganan terenkripsi Happ.",
|
||||
"subProfileUrl": "URL Profil",
|
||||
"subProfileUrlDesc": "Tautan ke situs web Anda yang ditampilkan di klien VPN. Mendukung token identitas klien: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "Tautan ke situs web Anda yang ditampilkan di klien VPN. Mendukung token identitas klien: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. Biarkan kosong agar tautan situs web tidak ditampilkan di klien VPN.",
|
||||
"subAnnounce": "Pengumuman",
|
||||
"subAnnounceDesc": "Teks pengumuman yang ditampilkan di klien VPN. Mendukung token identitas klien: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "Direktori Tema Langganan",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "VPNクライアントに表示されるタイトル。クライアント識別トークンをサポートします: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}。",
|
||||
"subSupportUrl": "サポートURL",
|
||||
"subSupportUrlDesc": "VPNクライアントに表示されるテクニカルサポートへのリンク。クライアント識別トークンをサポートします: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}。",
|
||||
"subProfileMode": "プロフィールページ",
|
||||
"subProfileModeDesc": "VPNクライアントに表示するWebサイトへのリンクを選択します。",
|
||||
"subProfileModeNone": "リンクなし",
|
||||
"subProfileModeBuiltin": "組み込みのサブスクリプションページ",
|
||||
"subProfileModeCustom": "カスタムWebサイト",
|
||||
"subProfileBuiltinWarning": "このページでは、Happで暗号化されたサブスクリプションも含め、サブスクリプションURLとノード設定が公開されます。",
|
||||
"subProfileUrl": "プロフィールURL",
|
||||
"subProfileUrlDesc": "VPNクライアントに表示されるWebサイトへのリンク。クライアント識別トークンをサポートします: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}。",
|
||||
"subProfileUrlDesc": "VPNクライアントに表示されるWebサイトへのリンク。クライアント識別トークンをサポートします: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}。空欄にすると、VPNクライアントにWebサイトへのリンクを表示しません。",
|
||||
"subAnnounce": "お知らせ",
|
||||
"subAnnounceDesc": "VPNクライアントに表示されるお知らせのテキスト。クライアント識別トークンをサポートします: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}。",
|
||||
"subThemeDir": "サブスクリプションテーマディレクトリ",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "Título exibido no cliente VPN. Suporta tokens de identidade do cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "URL de Suporte",
|
||||
"subSupportUrlDesc": "Link de suporte técnico exibido no cliente VPN. Suporta tokens de identidade do cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "Página de perfil",
|
||||
"subProfileModeDesc": "Escolha qual link de site é exibido no cliente VPN.",
|
||||
"subProfileModeNone": "Sem link",
|
||||
"subProfileModeBuiltin": "Página de assinatura integrada",
|
||||
"subProfileModeCustom": "Site personalizado",
|
||||
"subProfileBuiltinWarning": "Esta página expõe as URLs de assinatura e as configurações dos nós, inclusive para assinaturas criptografadas do Happ.",
|
||||
"subProfileUrl": "URL de Perfil",
|
||||
"subProfileUrlDesc": "Um link para o seu site exibido no cliente VPN. Suporta tokens de identidade do cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "Um link para o seu site exibido no cliente VPN. Suporta tokens de identidade do cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. Deixe em branco para omitir o link do site no cliente VPN.",
|
||||
"subAnnounce": "Anúncio",
|
||||
"subAnnounceDesc": "O texto do anúncio exibido no cliente VPN. Suporta tokens de identidade do cliente: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "Diretório do tema de assinatura",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "Название подписки, которое видит клиент в VPN-клиенте. Поддерживает токены идентификации клиента: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "URL поддержки",
|
||||
"subSupportUrlDesc": "Ссылка на техническую поддержку, отображаемая в VPN-клиенте. Поддерживает токены идентификации клиента: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "Страница профиля",
|
||||
"subProfileModeDesc": "Выберите, какая ссылка на сайт будет отображаться в VPN-клиенте.",
|
||||
"subProfileModeNone": "Без ссылки",
|
||||
"subProfileModeBuiltin": "Встроенная страница подписки",
|
||||
"subProfileModeCustom": "Свой сайт",
|
||||
"subProfileBuiltinWarning": "Эта страница раскрывает URL-адреса подписок и конфигурации узлов, в том числе для зашифрованных подписок Happ.",
|
||||
"subProfileUrl": "URL профиля",
|
||||
"subProfileUrlDesc": "Ссылка на ваш сайт, отображаемая в VPN-клиенте. Поддерживает токены идентификации клиента: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "Ссылка на ваш сайт, отображаемая в VPN-клиенте. Поддерживает токены идентификации клиента: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. Оставьте поле пустым, чтобы не отображать ссылку на сайт в VPN-клиенте.",
|
||||
"subAnnounce": "Объявление",
|
||||
"subAnnounceDesc": "Текст объявления, отображаемый в VPN-клиенте. Поддерживает токены идентификации клиента: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "Каталог темы подписки",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "VPN istemcisinde gösterilen başlık. İstemci kimlik tokenlarını destekler: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "Destek URL'si",
|
||||
"subSupportUrlDesc": "VPN istemcisinde gösterilen teknik destek bağlantısı. İstemci kimlik tokenlarını destekler: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "Profil sayfası",
|
||||
"subProfileModeDesc": "VPN istemcisinde hangi web sitesi bağlantısının gösterileceğini seçin.",
|
||||
"subProfileModeNone": "Bağlantı yok",
|
||||
"subProfileModeBuiltin": "Yerleşik abonelik sayfası",
|
||||
"subProfileModeCustom": "Özel web sitesi",
|
||||
"subProfileBuiltinWarning": "Bu sayfa, Happ ile şifrelenmiş abonelikler dahil olmak üzere abonelik URL'lerini ve düğüm yapılandırmalarını açığa çıkarır.",
|
||||
"subProfileUrl": "Profil URL'si",
|
||||
"subProfileUrlDesc": "VPN istemcisinde görüntülenen web sitenize giden bağlantı. İstemci kimlik tokenlarını destekler: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "VPN istemcisinde görüntülenen web sitenize giden bağlantı. İstemci kimlik tokenlarını destekler: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. Web sitesi bağlantısının VPN istemcisinde gösterilmemesi için boş bırakın.",
|
||||
"subAnnounce": "Duyuru",
|
||||
"subAnnounceDesc": "VPN istemcisinde görüntülenen duyuru metni. İstemci kimlik tokenlarını destekler: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "Abonelik Tema Dizini",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "Назва, яка відображається у VPN-клієнті. Підтримує токени ідентифікації клієнта: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "URL підтримки",
|
||||
"subSupportUrlDesc": "Посилання на технічну підтримку, що відображається у VPN-клієнті. Підтримує токени ідентифікації клієнта: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "Сторінка профілю",
|
||||
"subProfileModeDesc": "Виберіть, яке посилання на вебсайт відображатиметься у VPN-клієнті.",
|
||||
"subProfileModeNone": "Без посилання",
|
||||
"subProfileModeBuiltin": "Вбудована сторінка підписки",
|
||||
"subProfileModeCustom": "Власний вебсайт",
|
||||
"subProfileBuiltinWarning": "Ця сторінка розкриває URL-адреси підписок і конфігурації вузлів, зокрема для зашифрованих підписок Happ.",
|
||||
"subProfileUrl": "URL профілю",
|
||||
"subProfileUrlDesc": "Посилання на ваш вебсайт, що відображається у VPN-клієнті. Підтримує токени ідентифікації клієнта: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "Посилання на ваш вебсайт, що відображається у VPN-клієнті. Підтримує токени ідентифікації клієнта: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. Залиште поле порожнім, щоб не відображати посилання на вебсайт у VPN-клієнті.",
|
||||
"subAnnounce": "Оголошення",
|
||||
"subAnnounceDesc": "Текст оголошення, що відображається у VPN-клієнті. Підтримує токени ідентифікації клієнта: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "Каталог теми підписки",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "Tiêu đề hiển thị trong ứng dụng VPN. Hỗ trợ token định danh khách hàng: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subSupportUrl": "URL Hỗ trợ",
|
||||
"subSupportUrlDesc": "Liên kết hỗ trợ kỹ thuật hiển thị trong ứng dụng VPN. Hỗ trợ token định danh khách hàng: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileMode": "Trang hồ sơ",
|
||||
"subProfileModeDesc": "Chọn liên kết trang web được hiển thị trong ứng dụng VPN.",
|
||||
"subProfileModeNone": "Không cung cấp liên kết",
|
||||
"subProfileModeBuiltin": "Trang đăng ký tích hợp",
|
||||
"subProfileModeCustom": "Trang web tùy chỉnh",
|
||||
"subProfileBuiltinWarning": "Trang này công khai URL đăng ký và cấu hình nút, kể cả đối với các đăng ký được mã hóa bằng Happ.",
|
||||
"subProfileUrl": "URL Hồ sơ",
|
||||
"subProfileUrlDesc": "Liên kết đến trang web của bạn hiển thị trong ứng dụng VPN. Hỗ trợ token định danh khách hàng: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subProfileUrlDesc": "Liên kết đến trang web của bạn hiển thị trong ứng dụng VPN. Hỗ trợ token định danh khách hàng: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}. Để trống để không hiển thị liên kết trang web trong ứng dụng VPN.",
|
||||
"subAnnounce": "Thông báo",
|
||||
"subAnnounceDesc": "Văn bản thông báo hiển thị trong ứng dụng VPN. Hỗ trợ token định danh khách hàng: {{EMAIL}}, {{ID}}, {{SHORT_ID}}, {{SUB_ID}}, {{TELEGRAM_ID}}.",
|
||||
"subThemeDir": "Thư mục giao diện Đăng ký",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "在 VPN 客户端中显示的标题。支持客户端身份令牌:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。",
|
||||
"subSupportUrl": "支持链接",
|
||||
"subSupportUrlDesc": "VPN 客户端中显示的技术支持链接。支持客户端身份令牌:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。",
|
||||
"subProfileMode": "资料页方式",
|
||||
"subProfileModeDesc": "选择在 VPN 客户端中提供的资料页入口。",
|
||||
"subProfileModeNone": "不提供",
|
||||
"subProfileModeBuiltin": "内置订阅页",
|
||||
"subProfileModeCustom": "自定义网站",
|
||||
"subProfileBuiltinWarning": "此页面会公开订阅地址和节点配置,HAPP 加密订阅也不例外。",
|
||||
"subProfileUrl": "个人资料链接",
|
||||
"subProfileUrlDesc": "VPN 客户端中显示的网站链接。支持客户端身份令牌:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。",
|
||||
"subProfileUrlDesc": "VPN 客户端中显示的网站链接。支持客户端身份令牌:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。留空则不在 VPN 客户端提供网站链接。",
|
||||
"subAnnounce": "公告",
|
||||
"subAnnounceDesc": "VPN 客户端中显示的公告文本。支持客户端身份令牌:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。",
|
||||
"subThemeDir": "订阅主题目录",
|
||||
|
||||
@@ -1200,8 +1200,14 @@
|
||||
"subTitleDesc": "在 VPN 客戶端中顯示的標題。支援用戶端身分權杖:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。",
|
||||
"subSupportUrl": "支援連結",
|
||||
"subSupportUrlDesc": "VPN 用戶端中顯示的技術支援連結。支援用戶端身分權杖:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。",
|
||||
"subProfileMode": "資料頁方式",
|
||||
"subProfileModeDesc": "選擇在 VPN 用戶端中提供的資料頁入口。",
|
||||
"subProfileModeNone": "不提供",
|
||||
"subProfileModeBuiltin": "內建訂閱頁",
|
||||
"subProfileModeCustom": "自訂網站",
|
||||
"subProfileBuiltinWarning": "此頁面會公開訂閱網址和節點設定,HAPP 加密訂閱也不例外。",
|
||||
"subProfileUrl": "個人資料連結",
|
||||
"subProfileUrlDesc": "VPN 用戶端中顯示的網站連結。支援用戶端身分權杖:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。",
|
||||
"subProfileUrlDesc": "VPN 用戶端中顯示的網站連結。支援用戶端身分權杖:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。留空則不在 VPN 用戶端提供網站連結。",
|
||||
"subAnnounce": "公告",
|
||||
"subAnnounceDesc": "VPN 用戶端中顯示的公告文字。支援用戶端身分權杖:{{EMAIL}}、{{ID}}、{{SHORT_ID}}、{{SUB_ID}}、{{TELEGRAM_ID}}。",
|
||||
"subThemeDir": "訂閱主題目錄",
|
||||
|
||||
Reference in New Issue
Block a user