diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/client/app.go | 729 | ||||
| -rwxr-xr-x | cmd/client/frontend/wailsjs/go/main/App.d.ts | 60 | ||||
| -rwxr-xr-x | cmd/client/frontend/wailsjs/go/main/App.js | 115 | ||||
| -rwxr-xr-x | cmd/client/frontend/wailsjs/go/models.ts | 319 | ||||
| -rw-r--r-- | cmd/client/frontend/wailsjs/runtime/package.json | 24 | ||||
| -rw-r--r-- | cmd/client/frontend/wailsjs/runtime/runtime.d.ts | 330 | ||||
| -rw-r--r-- | cmd/client/frontend/wailsjs/runtime/runtime.js | 298 | ||||
| -rw-r--r-- | cmd/client/kill_other.go | 16 | ||||
| -rw-r--r-- | cmd/client/kill_windows.go | 56 | ||||
| -rw-r--r-- | cmd/client/main.go | 108 | ||||
| -rw-r--r-- | cmd/client/wails.json | 7 | ||||
| -rw-r--r-- | cmd/installer/elevate_windows.go | 74 | ||||
| -rw-r--r-- | cmd/installer/main.go | 236 | ||||
| -rw-r--r-- | cmd/server/main.go | 36 |
14 files changed, 2408 insertions, 0 deletions
diff --git a/cmd/client/app.go b/cmd/client/app.go new file mode 100644 index 0000000..6d26318 --- /dev/null +++ b/cmd/client/app.go @@ -0,0 +1,729 @@ +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "os/exec" + "runtime" + "time" + + wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime" + + "vpnem/internal/config" + "vpnem/internal/engine" + "vpnem/internal/models" + "vpnem/internal/state" + syncpkg "vpnem/internal/sync" +) + +const Version = "2.0.11" + +// App is the Wails backend. +type App struct { + ctx context.Context + engine *engine.Engine + watchdog *engine.Watchdog + fetcher *syncpkg.Fetcher + updater *syncpkg.Updater + state *state.Store + log *engine.RingLog + + catalog *models.CatalogV2 + servers []models.Server + ruleSets []models.RuleSet + policy *models.RoutingPolicy + + recommendedServerIP string + recommendedNodeID string + recommendReason string + studioClients int +} + +// NewApp creates a new App instance. +func NewApp(dataDir, apiURL string) *App { + eng := engine.New(dataDir) + fetcher := syncpkg.NewFetcher(apiURL) + rl := engine.NewRingLog(200, dataDir) + return &App{ + engine: eng, + watchdog: engine.NewWatchdog(eng, engine.DefaultWatchdogConfig()), + fetcher: fetcher, + updater: syncpkg.NewUpdater(fetcher, Version, dataDir), + state: state.NewStore(dataDir), + log: rl, + } +} + +// startup is called when the app starts. Must not block — Wails UI won't render until this returns. +func (a *App) startup(ctx context.Context) { + a.ctx = ctx + _ = a.state.Load() + a.logEvent("vpnem " + Version + " started") + + // Sync + recommendation synchronously — UI won't render until done + if err := a.Sync(); err != nil { + a.logEvent("initial sync failed: " + err.Error()) + a.reportError("initial sync failed: " + err.Error()) + } else { + a.logEvent("initial sync ok") + } + + // Fetch recommendation synchronously — blocks until ready + a.fetchRecommendation() + + // Periodic refresh in background + go func() { + ticker := time.NewTicker(30 * time.Minute) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := a.Sync(); err != nil { + a.logEvent("sync failed: " + err.Error()) + } + a.fetchRecommendation() + case <-ctx.Done(): + return + } + } + }() +} + +// shutdown is called when the app closes. +func (a *App) shutdown(ctx context.Context) { + a.logEvent("shutting down") + a.watchdog.StopWatching() + if err := a.engine.Stop(); err != nil { + a.logEvent("engine stop error: " + err.Error()) + } + // Fallback: kill any orphaned sing-box + killSingbox() + _ = a.state.Save() + a.logEvent("shutdown complete") + a.log.Close() +} + +func killSingbox() { + if runtime.GOOS == "windows" { + exec.Command("taskkill", "/F", "/IM", "sing-box.exe").Run() + } else { + exec.Command("pkill", "-f", "sing-box").Run() + } +} + +func (a *App) logEvent(msg string) { + line := time.Now().Format("15:04:05") + " " + msg + a.log.Add(line) + log.Println(msg) +} + +// --- Wails bindings --- + +// Sync fetches servers and rulesets from the API. +func (a *App) Sync() error { + catalog, err := a.fetcher.FetchCatalog() + if err != nil { + return fmt.Errorf("sync catalog: %w", err) + } + a.catalog = catalog + a.servers = syncpkg.CatalogToServers(catalog) + + rsResp, err := a.fetcher.FetchRuleSets() + if err != nil { + return fmt.Errorf("sync rulesets: %w", err) + } + + dataDir := a.state.DataDir() + a.ruleSets, err = a.fetcher.DownloadRuleSets(rsResp.RuleSets, dataDir) + if err != nil { + return fmt.Errorf("download rule-sets: %w", err) + } + + policy, err := a.fetcher.FetchRoutingPolicy() + if err != nil { + return fmt.Errorf("sync routing policy: %w", err) + } + a.policy = policy + + a.state.SetLastSync(time.Now()) + _ = a.state.Save() + a.logEvent(fmt.Sprintf("synced: %d servers, %d rulesets, routing policy %s", len(a.servers), len(a.ruleSets), a.policy.Version)) + + // Notify frontend to refresh + if a.ctx != nil { + wailsRuntime.EventsEmit(a.ctx, "synced") + } + return nil +} + +func (a *App) applyProfile(serverTag, modeName string, logAction string) error { + server := a.findServer(serverTag) + if server == nil { + return fmt.Errorf("server not found: %s", serverTag) + } + mode := config.ModeByName(modeName) + if mode == nil { + return fmt.Errorf("mode not found: %s", modeName) + } + + serverIPs := syncpkg.ServerIPs(a.servers) + activeRuleSets := a.activeRuleSets(*mode) + customBypass := a.state.GetCustomBypass() + localProxyPort, err := engine.ResolveLocalProxyPort() + if err != nil { + return fmt.Errorf("allocate local proxy port: %w", err) + } + + a.logEvent(logAction + ": " + serverTag + " [" + modeName + "]") + + // Flush DNS cache before applying a new profile (Windows caches poisoned responses). + flushDNS() + + if err := a.engine.RestartFull(*server, *mode, activeRuleSets, serverIPs, customBypass, localProxyPort, a.policy); err != nil { + a.logEvent(logAction + " failed: " + err.Error()) + return err + } + + a.watchdog.StartWatching(*server, *mode, activeRuleSets, serverIPs, customBypass, localProxyPort, a.policy) + a.state.SetServer(serverTag) + a.state.SetMode(modeName) + a.state.SetLocalProxyPort(localProxyPort) + _ = a.state.Save() + a.logEvent("connected: " + serverTag) + + if a.ctx != nil { + wailsRuntime.EventsEmit(a.ctx, "connected", serverTag) + } + + // Report connection to server for recommendation tracking + go a.ReportConnection(serverTag, server.Server, a.extractNodeID(serverTag)) + + go a.validateConnection() + return nil +} + +// Connect starts the VPN with the given server and mode. +func (a *App) Connect(serverTag, modeName string) error { + return a.applyProfile(serverTag, modeName, "connecting") +} + +// ApplyProfile switches the active server/mode live when connected. +// If the engine is not running yet, it only persists the selection for the next connect. +func (a *App) ApplyProfile(serverTag, modeName string) error { + server := a.findServer(serverTag) + if server == nil { + return fmt.Errorf("server not found: %s", serverTag) + } + mode := config.ModeByName(modeName) + if mode == nil { + return fmt.Errorf("mode not found: %s", modeName) + } + + current := a.state.Get() + if !a.engine.IsRunning() { + a.state.SetServer(serverTag) + a.state.SetMode(modeName) + _ = a.state.Save() + a.logEvent("profile selected: " + serverTag + " [" + modeName + "]") + return nil + } + + if current.SelectedServer == serverTag && current.SelectedMode == modeName { + return nil + } + + a.watchdog.StopWatching() + if err := a.applyProfile(serverTag, modeName, "switching profile"); err != nil { + return err + } + + a.state.SetServer(serverTag) + a.state.SetMode(modeName) + _ = a.state.Save() + return nil +} + +func flushDNS() { + if runtime.GOOS == "windows" { + exec.Command("ipconfig", "/flushdns").Run() + log.Println("DNS cache flushed") + } +} + +func (a *App) validateConnection() { + time.Sleep(3 * time.Second) + if !a.engine.IsRunning() { + return + } + + localProxyPort := a.engine.LocalProxyPort() + if localProxyPort == 0 { + localProxyPort = a.state.Get().LocalProxyPort + } + client, err := engine.HTTPClientViaSOCKS5(config.LocalProxyHost, localProxyPort, 10*time.Second) + if err != nil { + a.logEvent("validation setup failed — " + err.Error()) + return + } + + mode := config.ModeByName(a.state.Get().SelectedMode) + requiresExitIP := mode != nil && engine.ModeRequiresExitIP(*mode) + + ip := getExitIP(client) + switch { + case ip != "": + a.logEvent("exit IP: " + ip) + case requiresExitIP: + a.logEvent("WARNING: could not verify exit IP") + default: + a.logEvent("validation: exit IP skipped for direct-final mode") + } + + statusCode, err := engine.ProbeBlockedSite(localProxyPort, engine.DefaultBlockedSiteProbeURL, 10*time.Second) + if err == nil { + a.logEvent(fmt.Sprintf("validation: rutracker.org → %d OK", statusCode)) + } else { + a.logEvent("validation: rutracker.org FAILED — " + err.Error()) + } +} + +// Disconnect stops the VPN and clears system proxy. +func (a *App) Disconnect() error { + // Report disconnect before stopping + if a.engine.IsRunning() { + st := a.state.Get() + server := a.findServer(st.SelectedServer) + if server != nil { + nodeID := a.extractNodeID(st.SelectedServer) + go a.ReportDisconnect(server.Server, nodeID) + } + } + + a.watchdog.StopWatching() + clearSystemProxy() + a.logEvent("disconnected") + return a.engine.Stop() +} + +// SetSystemProxy sets Windows system SOCKS5 proxy directly (no TUN needed). +// Fallback for when TUN/sing-box doesn't work with browser. +func (a *App) SetSystemProxy(serverTag string) error { + localProxyPort := a.engine.LocalProxyPort() + if localProxyPort == 0 { + localProxyPort = a.state.Get().LocalProxyPort + } + if !a.engine.IsRunning() || localProxyPort == 0 { + return fmt.Errorf("connect first to start the local proxy") + } + addr := fmt.Sprintf("%s:%d", config.LocalProxyHost, localProxyPort) + if runtime.GOOS == "windows" { + // Route Windows system proxy through the local SOCKS5 inbound. + exec.Command("reg", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings`, + "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "1", "/f").Run() + exec.Command("reg", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings`, + "/v", "ProxyServer", "/t", "REG_SZ", "/d", "socks="+addr, "/f").Run() + a.logEvent("system proxy set to local SOCKS5 inbound: " + addr) + } + return nil +} + +func clearSystemProxy() { + if runtime.GOOS == "windows" { + exec.Command("reg", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings`, + "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "0", "/f").Run() + log.Println("system proxy cleared") + } +} + +// GetServers returns the server list grouped by region. +func (a *App) GetServers() []models.Server { + if a.servers == nil { + return []models.Server{} + } + return a.servers +} + +// GetRecommendedServerTag returns the tag of the recommended MULTI server. +// Only MULTI tags are returned (vless-reality + hysteria2). SOCKS5-only tags are never recommended. +// If the recommended IP doesn't match any MULTI server, falls back to first MULTI. +func (a *App) GetRecommendedServerTag() string { + // If we have a recommended IP, try to find a MULTI server for it + if a.recommendedServerIP != "" && len(a.servers) > 0 { + // Find MULTI server matching recommended IP + for _, s := range a.servers { + if s.Server == a.recommendedServerIP && isMultiServer(s) { + a.logEvent("GetRecommendedServerTag: MATCH " + s.Tag) + return s.Tag + } + } + a.logEvent("GetRecommendedServerTag: no MULTI match for IP " + a.recommendedServerIP) + } + + // Fallback: pick first MULTI server from the list + for _, s := range a.servers { + if isMultiServer(s) { + a.logEvent("GetRecommendedServerTag: fallback to " + s.Tag) + return s.Tag + } + } + + a.logEvent("GetRecommendedServerTag: no MULTI servers found") + return "" +} + +// isMultiServer checks if a server is a MULTI node (split TCP/UDP routing). +func isMultiServer(s models.Server) bool { + return s.Type == "multi" || len(s.Companions) > 0 +} + +// IsServerRecommended checks if a given server tag matches the recommendation. +func (a *App) IsServerRecommended(tag string) bool { + recTag := a.GetRecommendedServerTag() + return recTag != "" && tag == recTag +} + +// GetRecommendationReason returns the human-readable reason for the recommendation. +func (a *App) GetRecommendationReason() string { + return a.recommendReason +} + +// GetCatalog returns the current canonical server catalog. +func (a *App) GetCatalog() *models.CatalogV2 { + if a.catalog == nil { + return &models.CatalogV2{Version: "uninitialized", Nodes: []models.CatalogNode{}} + } + return a.catalog +} + +// GetModes returns all available mode names. +func (a *App) GetModes() []string { + return config.ModeNames() +} + +// GetStatus returns the current connection status. +func (a *App) GetStatus() map[string]any { + st := a.state.Get() + localProxyPort := a.engine.LocalProxyPort() + if localProxyPort == 0 { + localProxyPort = st.LocalProxyPort + } + return map[string]any{ + "connected": a.engine.IsRunning(), + "server": st.SelectedServer, + "mode": st.SelectedMode, + "lastSync": st.LastSync, + "autoConnect": st.AutoConnect, + "localProxyHost": config.LocalProxyHost, + "localProxyPort": localProxyPort, + "localProxyURL": fmt.Sprintf("%s:%d", config.LocalProxyHost, localProxyPort), + "localProxyScheme": "socks5", + } +} + +// GetExitIP checks the actual exit IP through the proxy. +func (a *App) GetExitIP() string { + localProxyPort := a.engine.LocalProxyPort() + if localProxyPort == 0 { + localProxyPort = a.state.Get().LocalProxyPort + } + client, err := engine.HTTPClientViaSOCKS5(config.LocalProxyHost, localProxyPort, 5*time.Second) + if err != nil { + return "" + } + return getExitIP(client) +} + +func getExitIP(client *http.Client) string { + resp, err := client.Get("http://ifconfig.me/ip") + if err != nil { + return "" + } + defer resp.Body.Close() + buf := make([]byte, 64) + n, _ := resp.Body.Read(buf) + return string(buf[:n]) +} + +// SetAutoConnect updates the auto-connect setting. +func (a *App) SetAutoConnect(v bool) { + a.state.SetAutoConnect(v) + _ = a.state.Save() +} + +// GetRuleSets returns all rule-sets with their enabled status. +func (a *App) GetRuleSets() []map[string]any { + result := make([]map[string]any, 0) + for _, rs := range a.ruleSets { + enabled := !rs.Optional || a.state.IsRuleSetEnabled(rs.Tag) + result = append(result, map[string]any{ + "tag": rs.Tag, + "description": rs.Description, + "type": rs.Type, + "optional": rs.Optional, + "enabled": enabled, + }) + } + return result +} + +// SetRuleSetEnabled enables or disables an optional rule-set. +func (a *App) SetRuleSetEnabled(tag string, enabled bool) { + a.state.SetRuleSetEnabled(tag, enabled) + _ = a.state.Save() +} + +// GetBypassProcesses returns default + custom bypass processes. +func (a *App) GetBypassProcesses() map[string]any { + policy := config.EffectiveRoutingPolicy(a.policy) + return map[string]any{ + "default": policy.AlwaysDirectProcesses, + "custom": a.state.GetCustomBypass(), + } +} + +// AddBypassProcess adds a custom bypass process. +func (a *App) AddBypassProcess(name string) { + current := a.state.GetCustomBypass() + for _, p := range current { + if p == name { + return + } + } + a.state.SetCustomBypass(append(current, name)) + _ = a.state.Save() +} + +// RemoveBypassProcess removes a custom bypass process. +func (a *App) RemoveBypassProcess(name string) { + current := a.state.GetCustomBypass() + var filtered []string + for _, p := range current { + if p != name { + filtered = append(filtered, p) + } + } + a.state.SetCustomBypass(filtered) + _ = a.state.Save() +} + +// MeasureLatency pings all servers and returns sorted results. +func (a *App) MeasureLatency() []syncpkg.LatencyResult { + a.logEvent("measuring latency...") + results := syncpkg.MeasureLatency(a.servers, 3*time.Second) + for _, r := range results { + if r.Latency >= 0 { + a.logEvent(fmt.Sprintf(" %s: %dms", r.Tag, r.Latency)) + } + } + return results +} + +// GetLogs returns the last N log lines. +func (a *App) GetLogs() []string { + return a.log.Lines() +} + +// GetGeneratedConfig returns the current sing-box config JSON for diagnostics. +func (a *App) GetGeneratedConfig() string { + path := a.engine.ConfigPath() + data, err := os.ReadFile(path) + if err != nil { + return "" + } + return string(data) +} + +// CheckUpdate checks if a new version is available. +func (a *App) CheckUpdate() (*syncpkg.UpdateInfo, error) { + return a.updater.Check() +} + +// DownloadUpdate downloads the new binary. Returns "restart_pending" on success. +// The caller should then shut down Wails gracefully — the OS will restart the app. +func (a *App) DownloadUpdate() (string, error) { + result, err := a.updater.Download() + if err != nil { + a.logEvent("update download failed: " + err.Error()) + return "", err + } + if result == "restart_pending" { + a.logEvent("update installed, restarting...") + // Shut down Wails gracefully — the OS Scheduled Task will relaunch + go func() { + time.Sleep(2 * time.Second) + wailsRuntime.Quit(a.ctx) + }() + } + return result, nil +} + +// RandomNLServer picks a random non-RU server tag. +func (a *App) RandomNLServer() string { + var candidates []string + for _, s := range a.servers { + if s.Region != "RU" { + candidates = append(candidates, s.Tag) + } + } + if len(candidates) == 0 { + return "" + } + return candidates[rand.Intn(len(candidates))] +} + +// fetchRecommendation fetches a recommended server from the API. +// Server auto-detects client real IP from X-Forwarded-For header. +func (a *App) fetchRecommendation() { + a.logEvent("fetchRecommendation: starting") + + // Check if we have a recent recommendation + recServer, recNodeID, recTime := a.state.GetRecommendation() + if recServer != "" && time.Since(recTime) < 15*time.Minute { + a.recommendedServerIP = recServer + a.recommendedNodeID = recNodeID + a.recommendReason = "cached" + a.logEvent("fetchRecommendation: using cached " + recServer) + return + } + + // Server detects client IP via X-Forwarded-For + rec, err := a.fetcher.GetRecommendation() + if err != nil { + a.logEvent("fetchRecommendation: error — " + err.Error()) + // Fallback: keep stale recommendation if available + if recServer != "" { + a.recommendedServerIP = recServer + a.recommendedNodeID = recNodeID + a.recommendReason = "stale (server unreachable)" + a.logEvent("fetchRecommendation: using stale " + recServer) + } + return + } + + if rec.RecommendedServerIP == "" { + a.logEvent("fetchRecommendation: empty recommendation from server") + return + } + + a.recommendedServerIP = rec.RecommendedServerIP + a.recommendedNodeID = rec.RecommendedNodeID + a.recommendReason = rec.Reason + if rec.IsRebalance { + a.recommendReason += " (ребалансировка)" + } + + a.state.SetRecommendation(rec.RecommendedServerIP, rec.RecommendedNodeID) + _ = a.state.Save() + + a.logEvent("fetchRecommendation: set " + rec.RecommendedServerIP + " — " + rec.Reason) + if rec.LoadInfo != "" { + a.logEvent(" load: " + rec.LoadInfo) + } +} + +// GetRecommendation returns the current recommendation data. +func (a *App) GetRecommendation() map[string]any { + return map[string]any{ + "server_ip": a.recommendedServerIP, + "node_id": a.recommendedNodeID, + "reason": a.recommendReason, + "studio_clients": a.studioClients, + } +} + +// ReportConnection sends a connect notification to the server. +// Server auto-detects client real IP from X-Forwarded-For. +func (a *App) ReportConnection(serverTag, serverIP, nodeID string) { + a.logEvent("report connect: " + serverTag + " → " + serverIP) + + resp, err := a.fetcher.ReportConnect(serverIP, nodeID) + if err != nil { + a.logEvent("report connect error: " + err.Error()) + return + } + + // Update recommendation if server returned a different one + if resp.RecommendedServerIP != "" && resp.RecommendedServerIP != a.recommendedServerIP { + old := a.recommendedServerIP + a.recommendedServerIP = resp.RecommendedServerIP + a.recommendedNodeID = resp.RecommendedNodeID + a.recommendReason = resp.Reason + if resp.IsRebalance { + a.recommendReason += " (ребалансировка)" + } + a.state.SetRecommendation(resp.RecommendedServerIP, resp.RecommendedNodeID) + _ = a.state.Save() + a.logEvent("recommendation updated: " + old + " → " + resp.RecommendedServerIP) + } +} + +// ReportDisconnect notifies the server of disconnection. +func (a *App) ReportDisconnect(serverIP, nodeID string) { + a.logEvent("report disconnect: " + serverIP) + if err := a.fetcher.ReportDisconnect(serverIP, nodeID); err != nil { + a.logEvent("report disconnect error: " + err.Error()) + } +} + +func (a *App) extractNodeID(serverTag string) string { + // Tags like "nl-198-multi", "nl-198-vless-reality", "nl-198-socks5" -> "nl-198" + for _, suffix := range []string{"-multi", "-vless-reality", "-vless", "-vmess", "-shadowsocks", "-hysteria2", "-socks5", "-socks"} { + if len(serverTag) > len(suffix) && serverTag[len(serverTag)-len(suffix):] == suffix { + return serverTag[:len(serverTag)-len(suffix)] + } + } + return serverTag +} + +func (a *App) findServer(tag string) *models.Server { + for _, s := range a.servers { + if s.Tag == tag { + return &s + } + } + return nil +} + +// activeRuleSets returns rule-sets for the mode PLUS domain rule-sets +// always needed for DNS anti-poisoning (refilter-domains etc). +func (a *App) activeRuleSets(mode config.Mode) []models.RuleSet { + needed := make(map[string]bool) + + // Rule-sets referenced by route rules + for _, r := range mode.Rules { + for _, tag := range r.RuleSet { + needed[tag] = true + } + } + + // Always include domain-type rule-sets for DNS rules + // (prevents ISP DNS poisoning for blocked domains) + for _, rs := range a.ruleSets { + if !rs.Optional && rs.Type == "domain" { + needed[rs.Tag] = true + } + } + + var result []models.RuleSet + for _, rs := range a.ruleSets { + if needed[rs.Tag] { + result = append(result, rs) + } + } + return result +} + +func (a *App) reportError(msg string) { + if a.fetcher == nil { + return + } + osName := "linux" + if runtime.GOOS == "windows" { + osName = "windows" + } + go a.fetcher.ReportError(Version, osName, a.log.Lines()) +} diff --git a/cmd/client/frontend/wailsjs/go/main/App.d.ts b/cmd/client/frontend/wailsjs/go/main/App.d.ts new file mode 100755 index 0000000..9f3f061 --- /dev/null +++ b/cmd/client/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,60 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {sync} from '../models'; +import {models} from '../models'; + +export function AddBypassProcess(arg1:string):Promise<void>; + +export function ApplyProfile(arg1:string,arg2:string):Promise<void>; + +export function CheckUpdate():Promise<sync.UpdateInfo>; + +export function Connect(arg1:string,arg2:string):Promise<void>; + +export function Disconnect():Promise<void>; + +export function DownloadUpdate():Promise<string>; + +export function GetBypassProcesses():Promise<Record<string, any>>; + +export function GetCatalog():Promise<models.CatalogV2>; + +export function GetExitIP():Promise<string>; + +export function GetGeneratedConfig():Promise<string>; + +export function GetLogs():Promise<Array<string>>; + +export function GetModes():Promise<Array<string>>; + +export function GetRecommendation():Promise<Record<string, any>>; + +export function GetRecommendationReason():Promise<string>; + +export function GetRecommendedServerTag():Promise<string>; + +export function GetRuleSets():Promise<Array<Record<string, any>>>; + +export function GetServers():Promise<Array<models.Server>>; + +export function GetStatus():Promise<Record<string, any>>; + +export function IsServerRecommended(arg1:string):Promise<boolean>; + +export function MeasureLatency():Promise<Array<sync.LatencyResult>>; + +export function RandomNLServer():Promise<string>; + +export function RemoveBypassProcess(arg1:string):Promise<void>; + +export function ReportConnection(arg1:string,arg2:string,arg3:string):Promise<void>; + +export function ReportDisconnect(arg1:string,arg2:string):Promise<void>; + +export function SetAutoConnect(arg1:boolean):Promise<void>; + +export function SetRuleSetEnabled(arg1:string,arg2:boolean):Promise<void>; + +export function SetSystemProxy(arg1:string):Promise<void>; + +export function Sync():Promise<void>; diff --git a/cmd/client/frontend/wailsjs/go/main/App.js b/cmd/client/frontend/wailsjs/go/main/App.js new file mode 100755 index 0000000..e85beea --- /dev/null +++ b/cmd/client/frontend/wailsjs/go/main/App.js @@ -0,0 +1,115 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function AddBypassProcess(arg1) { + return window['go']['main']['App']['AddBypassProcess'](arg1); +} + +export function ApplyProfile(arg1, arg2) { + return window['go']['main']['App']['ApplyProfile'](arg1, arg2); +} + +export function CheckUpdate() { + return window['go']['main']['App']['CheckUpdate'](); +} + +export function Connect(arg1, arg2) { + return window['go']['main']['App']['Connect'](arg1, arg2); +} + +export function Disconnect() { + return window['go']['main']['App']['Disconnect'](); +} + +export function DownloadUpdate() { + return window['go']['main']['App']['DownloadUpdate'](); +} + +export function GetBypassProcesses() { + return window['go']['main']['App']['GetBypassProcesses'](); +} + +export function GetCatalog() { + return window['go']['main']['App']['GetCatalog'](); +} + +export function GetExitIP() { + return window['go']['main']['App']['GetExitIP'](); +} + +export function GetGeneratedConfig() { + return window['go']['main']['App']['GetGeneratedConfig'](); +} + +export function GetLogs() { + return window['go']['main']['App']['GetLogs'](); +} + +export function GetModes() { + return window['go']['main']['App']['GetModes'](); +} + +export function GetRecommendation() { + return window['go']['main']['App']['GetRecommendation'](); +} + +export function GetRecommendationReason() { + return window['go']['main']['App']['GetRecommendationReason'](); +} + +export function GetRecommendedServerTag() { + return window['go']['main']['App']['GetRecommendedServerTag'](); +} + +export function GetRuleSets() { + return window['go']['main']['App']['GetRuleSets'](); +} + +export function GetServers() { + return window['go']['main']['App']['GetServers'](); +} + +export function GetStatus() { + return window['go']['main']['App']['GetStatus'](); +} + +export function IsServerRecommended(arg1) { + return window['go']['main']['App']['IsServerRecommended'](arg1); +} + +export function MeasureLatency() { + return window['go']['main']['App']['MeasureLatency'](); +} + +export function RandomNLServer() { + return window['go']['main']['App']['RandomNLServer'](); +} + +export function RemoveBypassProcess(arg1) { + return window['go']['main']['App']['RemoveBypassProcess'](arg1); +} + +export function ReportConnection(arg1, arg2, arg3) { + return window['go']['main']['App']['ReportConnection'](arg1, arg2, arg3); +} + +export function ReportDisconnect(arg1, arg2) { + return window['go']['main']['App']['ReportDisconnect'](arg1, arg2); +} + +export function SetAutoConnect(arg1) { + return window['go']['main']['App']['SetAutoConnect'](arg1); +} + +export function SetRuleSetEnabled(arg1, arg2) { + return window['go']['main']['App']['SetRuleSetEnabled'](arg1, arg2); +} + +export function SetSystemProxy(arg1) { + return window['go']['main']['App']['SetSystemProxy'](arg1); +} + +export function Sync() { + return window['go']['main']['App']['Sync'](); +} diff --git a/cmd/client/frontend/wailsjs/go/models.ts b/cmd/client/frontend/wailsjs/go/models.ts new file mode 100755 index 0000000..0f8dc72 --- /dev/null +++ b/cmd/client/frontend/wailsjs/go/models.ts @@ -0,0 +1,319 @@ +export namespace models { + + export class CatalogAuth { + uuid?: string; + method?: string; + password?: string; + + static createFrom(source: any = {}) { + return new CatalogAuth(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.uuid = source["uuid"]; + this.method = source["method"]; + this.password = source["password"]; + } + } + export class Reality { + enabled?: boolean; + public_key?: string; + private_key?: string; + short_id?: string; + fingerprint?: string; + + static createFrom(source: any = {}) { + return new Reality(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.enabled = source["enabled"]; + this.public_key = source["public_key"]; + this.private_key = source["private_key"]; + this.short_id = source["short_id"]; + this.fingerprint = source["fingerprint"]; + } + } + export class TLS { + enabled: boolean; + server_name?: string; + insecure?: boolean; + alpn?: string[]; + min_version?: string; + max_version?: string; + reality?: Reality; + + static createFrom(source: any = {}) { + return new TLS(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.enabled = source["enabled"]; + this.server_name = source["server_name"]; + this.insecure = source["insecure"]; + this.alpn = source["alpn"]; + this.min_version = source["min_version"]; + this.max_version = source["max_version"]; + this.reality = this.convertValues(source["reality"], Reality); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class CatalogProtocol { + type: string; + enabled: boolean; + port: number; + tls?: TLS; + auth?: CatalogAuth; + extra?: Record<string, any>; + + static createFrom(source: any = {}) { + return new CatalogProtocol(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.type = source["type"]; + this.enabled = source["enabled"]; + this.port = source["port"]; + this.tls = this.convertValues(source["tls"], TLS); + this.auth = this.convertValues(source["auth"], CatalogAuth); + this.extra = source["extra"]; + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class CatalogNode { + id: string; + name: string; + provider?: string; + region: string; + host: string; + domain?: string; + public_host: string; + protocols: CatalogProtocol[]; + status?: string; + tags?: string[]; + metadata?: Record<string, any>; + + static createFrom(source: any = {}) { + return new CatalogNode(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.name = source["name"]; + this.provider = source["provider"]; + this.region = source["region"]; + this.host = source["host"]; + this.domain = source["domain"]; + this.public_host = source["public_host"]; + this.protocols = this.convertValues(source["protocols"], CatalogProtocol); + this.status = source["status"]; + this.tags = source["tags"]; + this.metadata = source["metadata"]; + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + export class CatalogV2 { + version: string; + nodes: CatalogNode[]; + + static createFrom(source: any = {}) { + return new CatalogV2(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.version = source["version"]; + this.nodes = this.convertValues(source["nodes"], CatalogNode); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + export class Transport { + type?: string; + path?: string; + + static createFrom(source: any = {}) { + return new Transport(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.type = source["type"]; + this.path = source["path"]; + } + } + export class Server { + tag: string; + region: string; + type: string; + server: string; + server_port: number; + udp_over_tcp?: boolean; + uuid?: string; + method?: string; + password?: string; + obfs_password?: string; + up_mbps?: number; + down_mbps?: number; + tls?: TLS; + transport?: Transport; + companions?: Server[]; + + static createFrom(source: any = {}) { + return new Server(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.tag = source["tag"]; + this.region = source["region"]; + this.type = source["type"]; + this.server = source["server"]; + this.server_port = source["server_port"]; + this.udp_over_tcp = source["udp_over_tcp"]; + this.uuid = source["uuid"]; + this.method = source["method"]; + this.password = source["password"]; + this.obfs_password = source["obfs_password"]; + this.up_mbps = source["up_mbps"]; + this.down_mbps = source["down_mbps"]; + this.tls = this.convertValues(source["tls"], TLS); + this.transport = this.convertValues(source["transport"], Transport); + this.companions = this.convertValues(source["companions"], Server); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + +} + +export namespace sync { + + export class LatencyResult { + tag: string; + region: string; + latency_ms: number; + + static createFrom(source: any = {}) { + return new LatencyResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.tag = source["tag"]; + this.region = source["region"]; + this.latency_ms = source["latency_ms"]; + } + } + export class UpdateInfo { + available: boolean; + version: string; + changelog: string; + current_version: string; + + static createFrom(source: any = {}) { + return new UpdateInfo(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.available = source["available"]; + this.version = source["version"]; + this.changelog = source["changelog"]; + this.current_version = source["current_version"]; + } + } + +} + diff --git a/cmd/client/frontend/wailsjs/runtime/package.json b/cmd/client/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/cmd/client/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony <lea.anthony@gmail.com>", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/cmd/client/frontend/wailsjs/runtime/runtime.d.ts b/cmd/client/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..3bbea84 --- /dev/null +++ b/cmd/client/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,330 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise<boolean>; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise<Size>; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise<Position>; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise<boolean>; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise<boolean>; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise<boolean>; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise<Screen[]>; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise<EnvironmentInfo>; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise<string>; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise<boolean>; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void + +// Notification types +export interface NotificationOptions { + id: string; + title: string; + subtitle?: string; // macOS and Linux only + body?: string; + categoryId?: string; + data?: { [key: string]: any }; +} + +export interface NotificationAction { + id?: string; + title?: string; + destructive?: boolean; // macOS-specific +} + +export interface NotificationCategory { + id?: string; + actions?: NotificationAction[]; + hasReplyField?: boolean; + replyPlaceholder?: string; + replyButtonTitle?: string; +} + +// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) +// Initializes the notification service for the application. +// This must be called before sending any notifications. +export function InitializeNotifications(): Promise<void>; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise<void>; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise<boolean>; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise<boolean>; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise<boolean>; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise<void>; + +// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) +// Sends a notification with action buttons. Requires a registered category. +export function SendNotificationWithActions(options: NotificationOptions): Promise<void>; + +// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) +// Registers a notification category that can be used with SendNotificationWithActions. +export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise<void>; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise<void>; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise<void>; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise<void>; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise<void>; + +// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) +// Removes a notification by its identifier (cross-platform convenience function). +export function RemoveNotification(identifier: string): Promise<void>;
\ No newline at end of file diff --git a/cmd/client/frontend/wailsjs/runtime/runtime.js b/cmd/client/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..556621e --- /dev/null +++ b/cmd/client/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,298 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} + +export function InitializeNotifications() { + return window.runtime.InitializeNotifications(); +} + +export function CleanupNotifications() { + return window.runtime.CleanupNotifications(); +} + +export function IsNotificationAvailable() { + return window.runtime.IsNotificationAvailable(); +} + +export function RequestNotificationAuthorization() { + return window.runtime.RequestNotificationAuthorization(); +} + +export function CheckNotificationAuthorization() { + return window.runtime.CheckNotificationAuthorization(); +} + +export function SendNotification(options) { + return window.runtime.SendNotification(options); +} + +export function SendNotificationWithActions(options) { + return window.runtime.SendNotificationWithActions(options); +} + +export function RegisterNotificationCategory(category) { + return window.runtime.RegisterNotificationCategory(category); +} + +export function RemoveNotificationCategory(categoryId) { + return window.runtime.RemoveNotificationCategory(categoryId); +} + +export function RemoveAllPendingNotifications() { + return window.runtime.RemoveAllPendingNotifications(); +} + +export function RemovePendingNotification(identifier) { + return window.runtime.RemovePendingNotification(identifier); +} + +export function RemoveAllDeliveredNotifications() { + return window.runtime.RemoveAllDeliveredNotifications(); +} + +export function RemoveDeliveredNotification(identifier) { + return window.runtime.RemoveDeliveredNotification(identifier); +} + +export function RemoveNotification(identifier) { + return window.runtime.RemoveNotification(identifier); +}
\ No newline at end of file diff --git a/cmd/client/kill_other.go b/cmd/client/kill_other.go new file mode 100644 index 0000000..75ac0a5 --- /dev/null +++ b/cmd/client/kill_other.go @@ -0,0 +1,16 @@ +//go:build !windows + +package main + +import ( + "log" + "os/exec" + "time" +) + +func killPrevious() { + log.Println("killing previous instances") + exec.Command("pkill", "-f", "sing-box").Run() + // Don't pkill vpnem — we ARE vpnem + time.Sleep(500 * time.Millisecond) +} diff --git a/cmd/client/kill_windows.go b/cmd/client/kill_windows.go new file mode 100644 index 0000000..ba8c75a --- /dev/null +++ b/cmd/client/kill_windows.go @@ -0,0 +1,56 @@ +//go:build windows + +package main + +import ( + "log" + "os" + "os/exec" + "strconv" + "time" +) + +func killPrevious() { + myPID := os.Getpid() + log.Printf("killing previous instances (my PID: %d)", myPID) + + // Kill other vpnem.exe processes (not ourselves) + // Use WMIC to find PIDs, then taskkill each except ours + out, _ := exec.Command("wmic", "process", "where", + "name='vpnem.exe'", "get", "processid", "/format:list").Output() + for _, line := range splitLines(string(out)) { + if len(line) > 10 && line[:10] == "ProcessId=" { + pidStr := line[10:] + pid, err := strconv.Atoi(pidStr) + if err == nil && pid != myPID { + log.Printf("killing old vpnem.exe PID %d", pid) + exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)).Run() + } + } + } + + // Kill any orphaned sing-box.exe + exec.Command("taskkill", "/F", "/IM", "sing-box.exe").Run() + time.Sleep(500 * time.Millisecond) +} + +func splitLines(s string) []string { + var lines []string + start := 0 + for i := 0; i < len(s); i++ { + if s[i] == '\n' || s[i] == '\r' { + line := s[start:i] + if len(line) > 0 && line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + if line != "" { + lines = append(lines, line) + } + start = i + 1 + } + } + if start < len(s) { + lines = append(lines, s[start:]) + } + return lines +} diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..a2cabbf --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "embed" + "flag" + "log" + "os" + "path/filepath" + "runtime" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed frontend/dist +var assets embed.FS + +func main() { + apiURL := flag.String("api", "https://vpn.em-sysadmin.xyz", "API server URL") + dataDir := flag.String("data", defaultDataDir(), "data directory") + flag.Parse() + + // Ensure data dir exists + os.MkdirAll(*dataDir, 0o755) + + // Setup file logging so we always have diagnostics + logPath := filepath.Join(*dataDir, "vpnem.log") + logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err == nil { + log.SetOutput(logFile) + defer logFile.Close() + } + + log.Printf("vpnem starting, data=%s, api=%s, os=%s", *dataDir, *apiURL, runtime.GOOS) + + // Kill previous instances of vpnem and sing-box + killPrevious() + + // Clean stale update artifacts from crashed updates + if runtime.GOOS == "windows" { + exe, _ := os.Executable() + os.Remove(exe + ".old") // leftover from failed update + os.Remove(exe + ".tmp") // leftover temp + os.Remove(filepath.Join(*dataDir, "vpnem-new.exe")) + } else { + exe, _ := os.Executable() + os.Remove(exe + ".old") + os.Remove(filepath.Join(*dataDir, "vpnem-new")) + } + + // Check wintun.dll on Windows + if runtime.GOOS == "windows" { + exe, _ := os.Executable() + exeDir := filepath.Dir(exe) + wintunPaths := []string{ + filepath.Join(exeDir, "wintun.dll"), + filepath.Join(*dataDir, "wintun.dll"), + "wintun.dll", + } + found := false + for _, p := range wintunPaths { + if _, err := os.Stat(p); err == nil { + log.Printf("wintun.dll found: %s", p) + found = true + break + } + } + if !found { + log.Printf("WARNING: wintun.dll not found! TUN will fail. Searched: %v", wintunPaths) + } + } + + app := NewApp(*dataDir, *apiURL) + + log.Println("starting Wails UI") + if err := wails.Run(&options.App{ + Title: "vpnem", + Width: 480, + Height: 600, + MinWidth: 400, + MinHeight: 500, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + OnStartup: app.startup, + OnShutdown: app.shutdown, + Bind: []interface{}{ + app, + }, + }); err != nil { + log.Printf("FATAL wails error: %v", err) + // On Windows, also write to a visible error file + if runtime.GOOS == "windows" { + errPath := filepath.Join(*dataDir, "ERROR.txt") + os.WriteFile(errPath, []byte("vpnem failed to start:\n"+err.Error()+"\n\nCheck vpnem.log for details.\n"), 0o644) + } + os.Exit(1) + } +} + +func defaultDataDir() string { + if runtime.GOOS == "windows" { + return `C:\ProgramData\vpnem` + } + home, _ := os.UserHomeDir() + return filepath.Join(home, ".local", "share", "vpnem") +} diff --git a/cmd/client/wails.json b/cmd/client/wails.json new file mode 100644 index 0000000..0f64984 --- /dev/null +++ b/cmd/client/wails.json @@ -0,0 +1,7 @@ +{ + "name": "vpnem", + "outputfilename": "vpnem", + "author": { + "name": "sergei" + } +} diff --git a/cmd/installer/elevate_windows.go b/cmd/installer/elevate_windows.go new file mode 100644 index 0000000..f174390 --- /dev/null +++ b/cmd/installer/elevate_windows.go @@ -0,0 +1,74 @@ +//go:build windows + +package main + +import ( + "fmt" + "os" + "strings" + "syscall" + "unsafe" +) + +var ( + shell32 = syscall.NewLazyDLL("shell32.dll") + shellExecuteW = shell32.NewProc("ShellExecuteW") + isUserAnAdminProc = shell32.NewProc("IsUserAnAdmin") + errCancelledByUser = uintptr(1223) +) + +func isElevated() bool { + ret, _, _ := isUserAnAdminProc.Call() + return ret != 0 +} + +func relaunchElevated() error { + exePath, err := os.Executable() + if err != nil { + return err + } + + verb, err := syscall.UTF16PtrFromString("runas") + if err != nil { + return err + } + file, err := syscall.UTF16PtrFromString(exePath) + if err != nil { + return err + } + args, err := syscall.UTF16PtrFromString(joinWindowsArgs(os.Args[1:])) + if err != nil { + return err + } + + ret, _, callErr := shellExecuteW.Call( + 0, + uintptr(unsafe.Pointer(verb)), + uintptr(unsafe.Pointer(file)), + uintptr(unsafe.Pointer(args)), + 0, + 1, + ) + if ret <= 32 { + if ret == errCancelledByUser { + return fmt.Errorf("administrator privileges were not granted") + } + if callErr != syscall.Errno(0) { + return fmt.Errorf("ShellExecuteW failed: %w", callErr) + } + return fmt.Errorf("ShellExecuteW failed with code %d", ret) + } + return nil +} + +func joinWindowsArgs(args []string) string { + if len(args) == 0 { + return "" + } + quoted := make([]string, 0, len(args)) + for _, arg := range args { + escaped := strings.ReplaceAll(arg, `"`, `\"`) + quoted = append(quoted, `"`+escaped+`"`) + } + return strings.Join(quoted, " ") +} diff --git a/cmd/installer/main.go b/cmd/installer/main.go new file mode 100644 index 0000000..07f7fe8 --- /dev/null +++ b/cmd/installer/main.go @@ -0,0 +1,236 @@ +// vpnem-installer: Windows offline installer (GUI, no console window). +// Bundles all binaries — no network download needed. +// Installs to Program Files, creates Task Scheduler task for UAC-free launch. +// Requires admin (one-time). Supports silent mode: vpnem-installer.exe /S +// Cross-compiles from Linux with -ldflags "-H windowsgui" +package main + +import ( + "embed" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +//go:embed files/* +var bundledFiles embed.FS + +const ( + installDir = `C:\Program Files\vpnem` + dataDir = `C:\ProgramData\vpnem` + taskName = "vpnem" + + baseURL = "https://vpn.em-sysadmin.xyz/releases" + vpnemURL = baseURL + "/vpnem-windows-amd64.exe" +) + +var ( + silent bool + noShortcut bool + launch bool + logFile *os.File +) + +func main() { + // Parse flags + for _, arg := range os.Args[1:] { + a := strings.ToLower(strings.TrimLeft(arg, "/-")) + switch a { + case "s", "silent": + silent = true + case "noshortcut": + noShortcut = true + case "launch": + launch = true + } + } + + if !isElevated() { + if err := relaunchElevated(); err != nil { + fatal("request administrator privileges: %v", err) + } + return + } + + // 1. Kill ALL running instances immediately + step("stopping running instances") + exec.Command("taskkill", "/F", "/IM", "vpnem.exe").Run() + exec.Command("taskkill", "/F", "/IM", "sing-box.exe").Run() + time.Sleep(2 * time.Second) + + // 2. Remove OLD installation directories completely (clean slate) + step("removing old installation") + if err := os.RemoveAll(installDir); err != nil { + log.Printf("warning: could not remove old install dir: %v", err) + } + if err := os.RemoveAll(dataDir); err != nil { + log.Printf("warning: could not remove old data dir: %v", err) + } + // Also remove old ProxySwitcher if exists + os.RemoveAll(`C:\ProxySwitcher`) + + // 3. Create fresh directories + if err := os.MkdirAll(installDir, 0o755); err != nil { + fatal("create install dir: %v", err) + } + if err := os.MkdirAll(dataDir, 0o755); err != nil { + fatal("create data dir: %v", err) + } + + // Write bundled files instead of downloading + step("extracting bundled vpnem.exe") + if err := writeEmbedded("files/vpnem.exe", filepath.Join(installDir, "vpnem.exe")); err != nil { + fatal("extract vpnem.exe: %v", err) + } + step("extracting bundled sing-box.exe") + if err := writeEmbedded("files/sing-box.exe", filepath.Join(installDir, "sing-box.exe")); err != nil { + fatal("extract sing-box.exe: %v", err) + } + step("extracting bundled wintun.dll") + if err := writeEmbedded("files/wintun.dll", filepath.Join(installDir, "wintun.dll")); err != nil { + fatal("extract wintun.dll: %v", err) + } + + // Create Task Scheduler task — runs vpnem as admin WITHOUT UAC prompt. + // This is the key: task created by admin runs with highest privileges silently. + step("creating scheduled task (UAC-free launch)") + createTask() + + // Desktop shortcut — launches via schtasks (no UAC popup) + if !noShortcut { + step("creating desktop shortcut") + createShortcut() + } + + step("installation complete") + + if launch { + step("launching vpnem") + exec.Command("schtasks", "/Run", "/TN", taskName).Run() + } + + if !silent { + showDoneMessage() + } +} + +// createTask sets up a scheduled task that: +// 1. Runs vpnem.exe with highest privileges (no UAC) +// 2. Starts at logon with 15s delay (autostart) +// The same task is used for both manual launch and autostart. +func createTask() { + exec.Command("schtasks", "/Delete", "/TN", taskName, "/F").Run() + + // Create XML task definition for full control + exePath := filepath.Join(installDir, "vpnem.exe") + xml := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-16"?> +<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> + <RegistrationInfo> + <Description>vpnem VPN client</Description> + </RegistrationInfo> + <Triggers> + <LogonTrigger> + <Enabled>true</Enabled> + <Delay>PT15S</Delay> + </LogonTrigger> + </Triggers> + <Principals> + <Principal> + <LogonType>InteractiveToken</LogonType> + <RunLevel>HighestAvailable</RunLevel> + </Principal> + </Principals> + <Settings> + <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> + <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> + <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> + <ExecutionTimeLimit>PT0S</ExecutionTimeLimit> + <AllowStartOnDemand>true</AllowStartOnDemand> + <AllowHardTerminate>true</AllowHardTerminate> + </Settings> + <Actions> + <Exec> + <Command>%s</Command> + <Arguments>--data "%s"</Arguments> + <WorkingDirectory>%s</WorkingDirectory> + </Exec> + </Actions> +</Task>`, exePath, dataDir, installDir) + + xmlPath := filepath.Join(dataDir, "task.xml") + os.WriteFile(xmlPath, []byte(xml), 0o644) + + cmd := exec.Command("schtasks", "/Create", "/TN", taskName, "/XML", xmlPath, "/F") + out, err := cmd.CombinedOutput() + if err != nil { + log.Printf("task create failed: %v\n%s", err, string(out)) + } else { + log.Println("task created ok") + } + os.Remove(xmlPath) +} + +// createShortcut makes a desktop shortcut that runs the scheduled task (no UAC) +// IconLocation points to vpnem.exe so the shortcut has the app icon +func createShortcut() { + exePath := filepath.Join(installDir, "vpnem.exe") + ps := ` +$ws = New-Object -ComObject WScript.Shell +$s = $ws.CreateShortcut("$env:USERPROFILE\Desktop\vpnem.lnk") +$s.TargetPath = "schtasks.exe" +$s.Arguments = "/Run /TN vpnem" +$s.WorkingDirectory = "` + installDir + `" +$s.IconLocation = "` + exePath + `,0" +$s.Description = "vpnem VPN client" +$s.Save() +` + cmd := exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", ps) + if err := cmd.Run(); err != nil { + log.Printf("shortcut failed: %v (non-critical)", err) + } +} + +func step(msg string) { + log.Println(msg) +} + +func writeEmbedded(name, dest string) error { + data, err := bundledFiles.ReadFile(name) + if err != nil { + return fmt.Errorf("read embedded file %s: %w", name, err) + } + tmp := dest + ".tmp" + if err := os.WriteFile(tmp, data, 0o755); err != nil { + return fmt.Errorf("write %s: %w", dest, err) + } + info, _ := os.Stat(tmp) + log.Printf(" %s (%.1f MB)", filepath.Base(dest), float64(info.Size())/1024/1024) + return os.Rename(tmp, dest) +} + +func fatal(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + log.Printf("FATAL: %s", msg) + if silent { + os.Exit(1) + } + showError(msg) + os.Exit(1) +} + +func showDoneMessage() { + msg := fmt.Sprintf("vpnem installed to %s\\n\\nDesktop shortcut created.\\nAutostart at logon enabled.\\n\\nNo admin prompts needed to launch.", installDir) + exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", + fmt.Sprintf(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show("%s", "vpnem installer", "OK", "Information")`, msg), + ).Run() +} + +func showError(msg string) { + exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", + fmt.Sprintf(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show("Installation failed:\n%s", "vpnem installer", "OK", "Error")`, msg), + ).Run() +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..4382c70 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "log" + "net/http" + + "vpnem/internal/api" + "vpnem/internal/rules" +) + +func main() { + addr := flag.String("addr", ":8090", "listen address") + dataDir := flag.String("data", "./data", "path to data directory") + flag.Parse() + + store := rules.NewStore(*dataDir) + + // Verify data loads on startup + if _, err := store.LoadServers(); err != nil { + log.Fatalf("failed to load servers.json: %v", err) + } + if _, err := store.LoadRuleSets(); err != nil { + log.Fatalf("failed to load rulesets.json: %v", err) + } + if _, err := store.LoadVersion(); err != nil { + log.Fatalf("failed to load version.json: %v", err) + } + + router := api.NewRouter(store) + + log.Printf("vpnem server listening on %s", *addr) + if err := http.ListenAndServe(*addr, router); err != nil { + log.Fatal(err) + } +} |
