Release 4.0.1+1: remove account binding / centralized provisioning (no panel credentials in binary; configs via user's own subscription URL); add recommended-services block on About page; Windows 4.0.1.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-08-01 20:31:29 +03:00
co-authored by Cursor
parent 1144fab54d
commit 5f768c7732
26 changed files with 237 additions and 1255 deletions
+52 -95
View File
@@ -17,6 +17,7 @@ import (
"github.com/jchv/go-webview2"
"golang.org/x/sys/windows"
"vpnclient/internal/apphost"
"vpnclient/internal/appui"
"vpnclient/internal/config"
"vpnclient/internal/core"
@@ -40,31 +41,29 @@ type app struct {
}
type uiState struct {
Connected bool `json:"connected"`
Profile string `json:"profile,omitempty"`
ActiveProfile string `json:"active_profile,omitempty"`
Protocol string `json:"protocol,omitempty"`
HTTPProxy string `json:"http_proxy,omitempty"`
SOCKSProxy string `json:"socks_proxy,omitempty"`
SystemProxy bool `json:"system_proxy"`
Proxy string `json:"proxy"`
CoreReady bool `json:"core_ready"`
CorePath string `json:"core_path,omitempty"`
ConfigPath string `json:"config_path"`
Profiles []config.ProfileInfo `json:"profiles"`
Version string `json:"version"`
Update update.Status `json:"update"`
Pings []netcheck.Result `json:"pings"`
Subscription string `json:"subscription_url"`
SubInfo *config.SubscriptionInfo `json:"sub_info,omitempty"`
Remnawave remnawave.Settings `json:"remnawave"`
Hy2 core.Hy2Options `json:"hy2"`
StorePackaged bool `json:"store_packaged,omitempty"`
ProvisionReady bool `json:"provision_ready,omitempty"`
Account *remnawave.Account `json:"account,omitempty"`
Mode string `json:"mode"`
VPNSupported bool `json:"vpn_mode_supported"`
VPNActive bool `json:"vpn_active"`
Connected bool `json:"connected"`
Profile string `json:"profile,omitempty"`
ActiveProfile string `json:"active_profile,omitempty"`
Protocol string `json:"protocol,omitempty"`
HTTPProxy string `json:"http_proxy,omitempty"`
SOCKSProxy string `json:"socks_proxy,omitempty"`
SystemProxy bool `json:"system_proxy"`
Proxy string `json:"proxy"`
CoreReady bool `json:"core_ready"`
CorePath string `json:"core_path,omitempty"`
ConfigPath string `json:"config_path"`
Profiles []config.ProfileInfo `json:"profiles"`
Version string `json:"version"`
Update update.Status `json:"update"`
Pings []netcheck.Result `json:"pings"`
Subscription string `json:"subscription_url"`
SubInfo *config.SubscriptionInfo `json:"sub_info,omitempty"`
Remnawave remnawave.Settings `json:"remnawave"`
Hy2 core.Hy2Options `json:"hy2"`
StorePackaged bool `json:"store_packaged,omitempty"`
Mode string `json:"mode"`
VPNSupported bool `json:"vpn_mode_supported"`
VPNActive bool `json:"vpn_active"`
}
func main() {
@@ -154,38 +153,15 @@ func main() {
mustBind(w, "importSubscription", a.importSubscription)
mustBind(w, "saveRemnawave", a.saveRemnawave)
mustBind(w, "importRemnawave", a.importRemnawave)
mustBind(w, "provisionUser", a.provisionUser)
mustBind(w, "renewAccount", a.renewAccount)
mustBind(w, "setMode", a.setMode)
mustBind(w, "getLogs", a.getLogs)
go a.autoCheckUpdate()
go a.autoProvision()
mgr.StartAutoRenew(6 * time.Hour)
w.SetHtml(appui.IndexHTML)
w.Run()
}
// autoProvision quietly issues a panel account + imports configs on first run
// (NordVPN-style centralized provisioning — users never edit the panel).
func (a *app) autoProvision() {
if !a.mgr.ProvisionConfigured() {
return
}
time.Sleep(2 * time.Second)
out, err := a.mgr.EnsureAutoProvision()
if err != nil {
fmt.Fprintf(a.logBuf, "auto-provision: %v\n", err)
return
}
if out.Created {
fmt.Fprintf(a.logBuf, "auto-provision: создан %s · серверов %d\n", out.Username, out.Imported)
} else if out.Imported > 0 {
fmt.Fprintf(a.logBuf, "auto-provision: обновлены конфиги %s · серверов %d\n", out.Username, out.Imported)
}
}
func mustBind(w webview2.WebView, name string, fn interface{}) {
if err := w.Bind(name, fn); err != nil {
fatalDialog("bind %s: %v", name, err)
@@ -229,31 +205,29 @@ func (a *app) getState() (uiState, error) {
corePath, coreReady = path, true
}
out := uiState{
Connected: st.Connected,
Profile: st.Profile,
ActiveProfile: active,
Protocol: string(st.Protocol),
HTTPProxy: st.HTTPProxy,
SOCKSProxy: st.SOCKSProxy,
SystemProxy: cfg.SystemProxy,
Proxy: proxy,
CoreReady: coreReady,
CorePath: corePath,
ConfigPath: a.cfgPath,
Profiles: cfg.ListProfiles(),
Version: update.DisplayVersion(),
Update: a.updateStatus,
Pings: append([]netcheck.Result(nil), a.pings...),
Subscription: cfg.SubscriptionURL,
SubInfo: a.mgr.SubscriptionInfo(),
Remnawave: a.mgr.RemnawaveSettings(),
Hy2: a.mgr.ActiveHy2Options(),
StorePackaged: update.IsStorePackaged(),
ProvisionReady: a.mgr.ProvisionConfigured(),
Account: a.mgr.Account(),
Mode: a.mgr.Mode(),
VPNSupported: a.mgr.VPNModeSupported(),
VPNActive: st.VPNActive,
Connected: st.Connected,
Profile: st.Profile,
ActiveProfile: active,
Protocol: string(st.Protocol),
HTTPProxy: st.HTTPProxy,
SOCKSProxy: st.SOCKSProxy,
SystemProxy: cfg.SystemProxy,
Proxy: proxy,
CoreReady: coreReady,
CorePath: corePath,
ConfigPath: a.cfgPath,
Profiles: cfg.ListProfiles(),
Version: update.DisplayVersion(),
Update: a.updateStatus,
Pings: append([]netcheck.Result(nil), a.pings...),
Subscription: cfg.SubscriptionURL,
SubInfo: a.mgr.SubscriptionInfo(),
Remnawave: a.mgr.RemnawaveSettings(),
Hy2: a.mgr.ActiveHy2Options(),
StorePackaged: update.IsStorePackaged(),
Mode: a.mgr.Mode(),
VPNSupported: a.mgr.VPNModeSupported(),
VPNActive: st.VPNActive,
}
if out.Protocol == "" {
if p, err := cfg.ActiveProfile(); err == nil {
@@ -377,14 +351,6 @@ func (a *app) importRemnawave(s remnawave.Settings) (core.ImportResult, error) {
return a.mgr.ImportRemnawave(s)
}
func (a *app) provisionUser(username string) (core.ProvisionOutcome, error) {
return a.mgr.ProvisionAccess(username)
}
func (a *app) renewAccount() (core.ProvisionOutcome, error) {
return a.mgr.RenewAccount()
}
func (a *app) setMode(mode string) error {
return a.mgr.SetMode(strings.TrimSpace(mode))
}
@@ -551,21 +517,12 @@ func (a *app) autoCheckUpdate() {
}
func openURL(raw string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return fmt.Errorf("пустая ссылка")
// Shared allowlist: shop + «Рекомендуемые сервисы» hosts only.
normalized, err := apphost.LinkAllowed(raw)
if err != nil {
return err
}
lower := strings.ToLower(raw)
switch {
case lower == "https://evilfox.win/" || lower == "https://evilfox.win" ||
lower == "http://evilfox.win/" || lower == "http://evilfox.win":
raw = "https://evilfox.win/"
case strings.HasPrefix(lower, "https://evilfox.win/") || strings.HasPrefix(lower, "http://evilfox.win/"):
// allow shop deep-links on evilfox.win only
default:
return fmt.Errorf("разрешена только ссылка evilfox.win")
}
return shellOpen(raw)
return shellOpen(normalized)
}
func shellOpen(raw string) error {