diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index db457ec42d..6b98549de1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -184,14 +184,14 @@ jobs: sudo journalctl -u lanternd.service --since "$TEST_START_UTC" --no-pager > /tmp/lanternd-journal-ui-smoke.log - if ! grep -Eq 'IPC request.*path=/service/start' /tmp/lanternd-journal-ui-smoke.log; then - echo "Missing /service/start IPC request in lanternd journal" + if ! grep -Eq 'IPC request.*path=/vpn/connect' /tmp/lanternd-journal-ui-smoke.log; then + echo "Missing /vpn/connect IPC request in lanternd journal" tail -n 200 /tmp/lanternd-journal-ui-smoke.log || true exit 1 fi - if ! grep -Eq 'IPC request.*path=/service/stop' /tmp/lanternd-journal-ui-smoke.log; then - echo "Missing /service/stop IPC request in lanternd journal" + if ! grep -Eq 'IPC request.*path=/vpn/disconnect' /tmp/lanternd-journal-ui-smoke.log; then + echo "Missing /vpn/disconnect IPC request in lanternd journal" tail -n 200 /tmp/lanternd-journal-ui-smoke.log || true exit 1 fi diff --git a/Makefile b/Makefile index a19b4028cd..9ebc9a393e 100644 --- a/Makefile +++ b/Makefile @@ -292,7 +292,7 @@ linux: linux-amd64 linux-service-amd64: $(GO_SOURCES) $(call MKDIR_P,$(dir $(LINUX_SERVICE_BUILD_AMD64))) GOOS=linux GOARCH=amd64 CGO_ENABLED=1 \ - go build -v -trimpath -tags "$(TAGS)" \ + go build -mod=mod -v -trimpath -tags "$(TAGS)" \ -ldflags "-w -s $(EXTRA_LDFLAGS)" \ -o $(LINUX_SERVICE_BUILD_AMD64) $(LINUX_SERVICE_SRC) @echo "Built Linux service: $(LINUX_SERVICE_BUILD_AMD64)" @@ -300,7 +300,7 @@ linux-service-amd64: $(GO_SOURCES) linux-service-arm64: $(GO_SOURCES) $(call MKDIR_P,$(dir $(LINUX_SERVICE_BUILD_ARM64))) GOOS=linux GOARCH=arm64 CGO_ENABLED=1 \ - go build -v -trimpath -tags "$(TAGS)" \ + go build -mod=mod -v -trimpath -tags "$(TAGS)" \ -ldflags "-w -s $(EXTRA_LDFLAGS)" \ -o $(LINUX_SERVICE_BUILD_ARM64) $(LINUX_SERVICE_SRC) @echo "Built Linux service: $(LINUX_SERVICE_BUILD_ARM64)" diff --git a/go.mod b/go.mod index 47369c7f88..0e5836673a 100644 --- a/go.mod +++ b/go.mod @@ -23,9 +23,8 @@ replace github.com/refraction-networking/water => github.com/getlantern/water v0 require ( github.com/Microsoft/go-winio v0.6.2 github.com/alecthomas/assert/v2 v2.3.0 - github.com/getlantern/common v1.2.1-0.20260325181816-33f69c725899 github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 - github.com/getlantern/radiance v0.0.0-20260326160312-80e8b51cccce + github.com/getlantern/radiance v0.0.0-20260326173647-f4a826182215 github.com/sagernet/sing-box v1.12.22 golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0 golang.org/x/sys v0.41.0 @@ -168,6 +167,7 @@ require ( github.com/getlantern/algeneva v0.0.0-20250307163401-1824e7b54f52 // indirect github.com/getlantern/amp v0.0.0-20260305201851-782bc8045e58 // indirect github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 // indirect + github.com/getlantern/common v1.2.1-0.20260224184656-5aefb9c21c85 // indirect github.com/getlantern/dnstt v0.0.0-20260112160750-05100563bd0d // indirect github.com/getlantern/fronted v0.0.0-20260325003030-cb5041ba1538 // indirect github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae // indirect @@ -182,7 +182,6 @@ require ( github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect github.com/go-llsqlite/crawshaw v0.5.6-0.20250312230104-194977a03421 // indirect - github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/goccy/go-yaml v1.19.0 // indirect diff --git a/go.sum b/go.sum index 3f5c586c2a..cdf376afcd 100644 --- a/go.sum +++ b/go.sum @@ -227,8 +227,8 @@ github.com/getlantern/amp v0.0.0-20260305201851-782bc8045e58 h1:3wxMKw90adxiEzsJ github.com/getlantern/amp v0.0.0-20260305201851-782bc8045e58/go.mod h1:p6WdG48YAz5SCUpiMSGLy616A6YghKToc63y3NP7avI= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 h1:Mmeh4/DA1OKN9tVWRAvTL5efFx4c7v9/55hoK17NclA= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01/go.mod h1:3vR6+jQdWfWojZ77w+htCqEF5MO/Y2twJOpAvFuM9po= -github.com/getlantern/common v1.2.1-0.20260325181816-33f69c725899 h1:aOKtUREDeyZ9J5Yp0I6zVjSWuMtlLNyfykKQ53VLfTc= -github.com/getlantern/common v1.2.1-0.20260325181816-33f69c725899/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0= +github.com/getlantern/common v1.2.1-0.20260224184656-5aefb9c21c85 h1:jj2Qv6drsceI4v3krNHXSn805QDRqy7w4XMqW359Z18= +github.com/getlantern/common v1.2.1-0.20260224184656-5aefb9c21c85/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo= github.com/getlantern/dnstt v0.0.0-20260112160750-05100563bd0d h1:TrauJ2jdJqOAHyQB5wIL0kWN/dipqKagERE1I/TRVSY= @@ -259,8 +259,8 @@ github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 h1:JWH5BB2o0e github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175/go.mod h1:h3S9LBmmzN/xM+lwYZHE4abzTtCTtidKtG+nxZcCZX0= github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YLAuT8r51ApR5z0d8/qjhHu3TW+divQ2C98Ac= github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q= -github.com/getlantern/radiance v0.0.0-20260326160312-80e8b51cccce h1:ARU8+/fVmcz2AJY2SqsrqWV0BHMyY6nmoHGmvWD4PzQ= -github.com/getlantern/radiance v0.0.0-20260326160312-80e8b51cccce/go.mod h1:jn8+Y7vnyULW+8DX1iDp3qKVBa81EUWFVDvMvkaZrQE= +github.com/getlantern/radiance v0.0.0-20260326173647-f4a826182215 h1:yhwUvgUXZ+3h/zdm9paiSKpCO9bCLl29uTk5eaUdY2Y= +github.com/getlantern/radiance v0.0.0-20260326173647-f4a826182215/go.mod h1:UdHjy9sV/4XuVH/3WdAGBdi6hUIiFVsWaZWO6f/5fpY= github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60 h1:m9eXjDK9vllbVH467+QXbrxUFFM9Yp7YJ90wZLw4dwU= github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0= github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo= @@ -314,8 +314,6 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= -github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= diff --git a/ios/Tunnel/SingBox/ExtensionProvider.swift b/ios/Tunnel/SingBox/ExtensionProvider.swift index ef126a150f..66d327b489 100644 --- a/ios/Tunnel/SingBox/ExtensionProvider.swift +++ b/ios/Tunnel/SingBox/ExtensionProvider.swift @@ -29,6 +29,15 @@ class ExtensionProvider: NEPacketTunnelProvider { if platformInterface == nil { platformInterface = ExtensionPlatformInterface(self) } + + // Start the IPC server before any VPN operations + var ipcError: NSError? + MobileStartIPCServer(platformInterface, opts(), &ipcError) + if let ipcError { + appLogger.error("error starting IPC server: \(ipcError.localizedDescription)") + throw ipcError + } + let tunnelType = options?["netEx.Type"] as? String switch tunnelType { case "Lantern": @@ -89,9 +98,9 @@ class ExtensionProvider: NEPacketTunnelProvider { appLogger.log("(lantern-tunnel) stopping, reason: \(reason)") stopService() var error: NSError? - MobileCloseIPC(&error) + MobileCloseIPCServer(&error) if error != nil { - appLogger.log("error closing IPC \(error?.localizedDescription ?? "")") + appLogger.log("error closing IPC server \(error?.localizedDescription ?? "")") } let elapsed = Date().timeIntervalSince(startTime) appLogger.log("(lantern-tunnel) stopTunnel completed in \(elapsed) seconds") diff --git a/lantern-core/cmd/lanternsvc/main.go b/lantern-core/cmd/lanternsvc/main.go index 70a9541019..6730ac9609 100644 --- a/lantern-core/cmd/lanternsvc/main.go +++ b/lantern-core/cmd/lanternsvc/main.go @@ -46,7 +46,9 @@ func init() { func main() { // Initialize radiance to ensure our directories and logging are set up. - rcommon.InitReadOnly("", "", "trace") + if err := rcommon.Init("", "", "trace"); err != nil { + slog.Error("Failed to initialize radiance", "error", err) + } consoleMode := flag.Bool("console", false, "Run in console mode instead of Windows service") flag.Parse() diff --git a/lantern-core/core.go b/lantern-core/core.go index d5dbea6853..6ef7b326e3 100644 --- a/lantern-core/core.go +++ b/lantern-core/core.go @@ -5,20 +5,15 @@ import ( "encoding/json" "fmt" "log/slog" - "os" - "path/filepath" - "strconv" "strings" "sync" "sync/atomic" - "github.com/getlantern/radiance" - "github.com/getlantern/radiance/api" + "github.com/getlantern/radiance/account" "github.com/getlantern/radiance/common" "github.com/getlantern/radiance/common/env" "github.com/getlantern/radiance/common/settings" - "github.com/getlantern/radiance/config" - "github.com/getlantern/radiance/events" + "github.com/getlantern/radiance/ipc" "github.com/getlantern/radiance/issue" "github.com/getlantern/radiance/servers" "github.com/getlantern/radiance/vpn" @@ -31,21 +26,17 @@ import ( type EventType = string const ( - EventTypeConfig EventType = "config" EventTypeServerLocation EventType = "server-location" DefaultLogLevel = "trace" - - plansCacheFile = "plans-cache.json" ) -// LanternCore is the main structure accessing the Lantern backend. +// LanternCore wraps an IPC client and provides the interface expected by the FFI and mobile layers. type LanternCore struct { - rad *radiance.Radiance - splitTunnel *vpn.SplitTunnel - serverManager *servers.Manager - apiClient *api.APIClient - initOnce sync.Once - eventEmitter utils.FlutterEventEmitter + client *ipc.Client + ctx context.Context + cancel context.CancelFunc + initOnce sync.Once + eventEmitter utils.FlutterEventEmitter } var ( @@ -66,14 +57,12 @@ type App interface { StartBackgroundListeners() StopBackgroundListeners() UpdateTelemetryConsent(consent bool) error - GetAppDataDir() string GetEnabledApps() (string, error) } type User interface { UserData() ([]byte, error) DataCapInfo() (string, error) - DataCapStream(ctx context.Context) error FetchUserData() ([]byte, error) OAuthLoginUrl(provider string) (string, error) OAuthLoginCallback(oAuthToken string) ([]byte, error) @@ -85,8 +74,7 @@ type User interface { ValidateChangeEmailCode(email, code string) error CompleteRecoveryByEmail(email, password, code string) error DeleteAccount(email, password string, isOAuthUser bool) ([]byte, error) - RemoveDevice(deviceId string) (*api.LinkResponse, error) - //Change email + RemoveDevice(deviceId string) (*account.LinkResponse, error) StartChangeEmail(newEmail, password string) error CompleteChangeEmail(email, password, code string) error } @@ -115,7 +103,7 @@ type Payment interface { AcknowledgeApplePurchase(receipt, planII string) (string, error) PaymentRedirect(provider, planID, email string) (string, error) ActivationCode(email, resellerCode string) error - SubscriptionPaymentRedirectURL(redirectBody api.PaymentRedirectData) (string, error) + SubscriptionPaymentRedirectURL(redirectBody account.PaymentRedirectData) (string, error) StripeSubscriptionPaymentRedirect(subscriptionType, planID, email string) (string, error) } @@ -141,6 +129,12 @@ type SmartRouting interface { IsSmartRoutingEnabled() bool } +type VPN interface { + ConnectVPN(tag string) error + DisconnectVPN() error + VPNStatus() (vpn.VPNStatus, error) +} + type Core interface { App User @@ -149,9 +143,10 @@ type Core interface { SplitTunnel Ads SmartRouting + VPN + Client() *ipc.Client } -// Make sure LanternCore implements the Core interface var _ Core = (*LanternCore)(nil) func New(opts *utils.Opts, eventEmitter utils.FlutterEventEmitter) (Core, error) { @@ -159,9 +154,6 @@ func New(opts *utils.Opts, eventEmitter utils.FlutterEventEmitter) (Core, error) return nil, fmt.Errorf("opts and eventEmitter cannot be nil") } - // This isn't ideal, but currently on Android and maybe other platforms - // there are multiple places that try to initialize the backend, so we - // need to ensure it's only done once. core.initOnce.Do(func() { if opts.LogLevel == "" { opts.LogLevel = DefaultLogLevel @@ -180,213 +172,168 @@ func New(opts *utils.Opts, eventEmitter utils.FlutterEventEmitter) (Core, error) func (lc *LanternCore) initialize(opts *utils.Opts, eventEmitter utils.FlutterEventEmitter) error { slog.Debug("Starting LanternCore initialization") - // Set the environment before initializing Radiance so that common.Stage()/Prod()/Dev() - // pick up the correct value during initialization. + if opts.Env == "stage" || opts.Env == "staging" { slog.Debug("Setting staging environment") env.SetStagingEnv() } - var radErr error - if lc.rad, radErr = radiance.NewRadiance(radiance.Options{ - LogDir: opts.LogDir, - DataDir: opts.DataDir, - DeviceID: opts.Deviceid, - LogLevel: opts.LogLevel, - Locale: opts.Locale, - TelemetryConsent: opts.TelemetryConsent, - }); radErr != nil { - return fmt.Errorf("failed to create Radiance: %w", radErr) - } - slog.Debug("Paths:", "logs", settings.GetString(settings.LogPathKey), "data", settings.GetString(settings.DataPathKey)) - var sthErr error - if lc.splitTunnel, sthErr = vpn.NewSplitTunnelHandler(); sthErr != nil { - return fmt.Errorf("unable to create split tunnel handler: %v", sthErr) + ctx, cancel := context.WithCancel(context.Background()) + client, err := createClient(ctx, opts) + if err != nil { + cancel() + return fmt.Errorf("failed to create IPC client: %w", err) } - lc.serverManager = lc.rad.ServerManager() - lc.apiClient = lc.rad.APIHandler() + lc.client = client + lc.ctx = ctx + lc.cancel = cancel lc.eventEmitter = eventEmitter - // Listen for config updates and notify Flutter - events.Subscribe(func(evt config.NewConfigEvent) { - core.notifyFlutter(EventTypeConfig, "Config is fetched/updated") - }) + go lc.listenAutoSelectedEvents() + go lc.listenDataCapEvents() - lc.listeningServerLocationChanges() - lc.listeningDataCapChanges() slog.Debug("LanternCore initialized successfully") + return nil +} - // If we have a legacy user ID, fetch user data - if settings.GetInt64(settings.UserIDKey) != 0 { - userData, _ := core.FetchUserData() - slog.Debug("Fetched user data", "data", string(userData)) - } +func (lc *LanternCore) Client() *ipc.Client { + return lc.client +} - return nil +// notifyFlutter sends an event to the Flutter frontend via the event emitter. +func (lc *LanternCore) notifyFlutter(event EventType, message string) { + slog.Debug("Notifying Flutter") + lc.eventEmitter.SendEvent(&utils.FlutterEvent{ + Type: string(event), + Message: message, + }) } -// Listen for server location changes and notify Flutter -func (lc *LanternCore) listeningServerLocationChanges() { - events.Subscribe(func(evt vpn.AutoSelectionsEvent) { - tag := evt.Selections.Lantern - servers, ok := lc.serverManager.GetServerByTag(tag) - if !ok { - slog.Error("no server found with tag", "tag", tag) +func (lc *LanternCore) listenAutoSelectedEvents() { + err := lc.client.AutoSelectedEvents(lc.ctx, func(evt vpn.AutoSelectedEvent) { + server, found, err := lc.client.GetServerByTag(lc.ctx, evt.Selected) + if err != nil || !found { + slog.Error("no server found with tag", "tag", evt.Selected, "error", err) return } - jsonBytes, err := json.Marshal(servers) + jsonBytes, err := json.Marshal(server) if err != nil { slog.Error("Error marshalling server location", "error", err) return } - stringBody := string(jsonBytes) - slog.Debug("Auto location server:", "server", stringBody) - lc.notifyFlutter(EventTypeServerLocation, stringBody) + slog.Debug("Auto location server:", "server", string(jsonBytes)) + lc.notifyFlutter(EventTypeServerLocation, string(jsonBytes)) }) + if err != nil && lc.ctx.Err() == nil { + slog.Error("auto-selected event stream ended", "error", err) + } } -func (lc *LanternCore) listeningDataCapChanges() { - events.Subscribe(func(evt api.DataCapChangeEvent) { - dataCapResponse := evt.DataCapUsageResponse - jsonBytes, err := json.Marshal(dataCapResponse) + +func (lc *LanternCore) listenDataCapEvents() { + err := lc.client.DataCapStream(lc.ctx, func(info account.DataCapInfo) { + jsonBytes, err := json.Marshal(info) if err != nil { slog.Error("Error marshalling DataCap event", "error", err) return } - stringBody := string(jsonBytes) - slog.Debug("DataCap event:", "event", stringBody) - lc.notifyFlutter("data-cap-event", stringBody) + lc.notifyFlutter("data-cap-event", string(jsonBytes)) }) -} - -func (lc *LanternCore) UpdateTelemetryConsent(consent bool) error { - slog.Debug("Updating telemetry consent", "consent", consent) - if consent { - slog.Info("User has opted in to telemetry") - lc.rad.EnableTelemetry() - } else { - slog.Info("User has opted out of telemetry") - lc.rad.DisableTelemetry() + if err != nil && lc.ctx.Err() == nil { + slog.Error("datacap event stream ended", "error", err) } - return nil } -func (lc *LanternCore) SetSmartRoutingMode(mode bool) error { - slog.Debug("Setting Smart Routing Mode to:", "mode", mode) - if err := vpn.SetSmartRouting(mode); err != nil { - return fmt.Errorf("failed to set Smart Routing Mode: %w", err) - } - return nil -} +///////////////// +// VPN // +///////////////// -func (lc *LanternCore) GetSmartRoutingMode() bool { - return vpn.SmartRoutingEnabled() +func (lc *LanternCore) ConnectVPN(tag string) error { + return lc.client.ConnectVPN(lc.ctx, tag) } -// Internal methods -// notifyFlutter sends an event to the Flutter frontend via the event emitter. -// On mobile we use EventChannel; on desktop this goes over the FFI event port -func (lc *LanternCore) notifyFlutter(event EventType, message string) { - slog.Debug("Notifying Flutter") - lc.eventEmitter.SendEvent(&utils.FlutterEvent{ - Type: string(event), - Message: message, - }) +func (lc *LanternCore) DisconnectVPN() error { + return lc.client.DisconnectVPN(lc.ctx) } -type backgroundListenerManager struct { - cancel context.CancelFunc - isRunning bool - mu sync.Mutex +func (lc *LanternCore) VPNStatus() (vpn.VPNStatus, error) { + return lc.client.VPNStatus(lc.ctx) } -var listenerManager = &backgroundListenerManager{ - // avoid nil cancel - cancel: func() {}, -} - -func (lc *LanternCore) StartBackgroundListeners() { - slog.Info("Starting background listeners...") - listenerManager.mu.Lock() - defer listenerManager.mu.Unlock() - - if listenerManager.isRunning { - slog.Info("Background listeners already running") - return +func (lc *LanternCore) IsVPNRunning() (bool, error) { + status, err := lc.client.VPNStatus(lc.ctx) + if err != nil { + return false, err } + return status == vpn.Connected, nil +} - ctx, cancel := context.WithCancel(context.Background()) - listenerManager.cancel = cancel - listenerManager.isRunning = true - - // Auto location listener - go vpn.AutoSelectionsChangeListener(ctx) - - // DataCap SSE stream - go func() { - if err := lc.apiClient.DataCapStream(ctx); err != nil { - slog.Error("datacap stopped", "error", err) - } - }() +///////////////// +// Settings // +///////////////// - slog.Info("Background listeners started") +func (lc *LanternCore) UpdateTelemetryConsent(consent bool) error { + return lc.client.EnableTelemetry(lc.ctx, consent) } -// stopAutoLocationListener stops the location listener - -func (lc *LanternCore) StopBackgroundListeners() { - slog.Info("Stopping background listeners...") - listenerManager.mu.Lock() - defer listenerManager.mu.Unlock() - if !listenerManager.isRunning { - slog.Info("Background listeners not running") - return - } - listenerManager.cancel() - listenerManager.isRunning = false - slog.Info("Background listeners stopped") +func (lc *LanternCore) SetBlockAdsEnabled(enabled bool) error { + return lc.client.EnableAdBlocking(lc.ctx, enabled) } -// GetServerByTagJSON returns the server for a given tag as pre-marshalled JSON. -// This is safe to call from CGo callback stacks because the pointer-rich Server -// types are marshalled here rather than being returned to the caller. -func (lc *LanternCore) GetServerByTagJSON(tag string) ([]byte, bool, error) { - return lc.serverManager.GetServerByTagJSON(tag) +func (lc *LanternCore) IsBlockAdsEnabled() bool { + s, err := lc.client.Settings(lc.ctx) + if err != nil { + return false + } + v, ok := s[settings.AdBlockKey] + if !ok { + return false + } + b, _ := v.(bool) + return b } -func (lc *LanternCore) VPNStatus() (vpn.Status, error) { - return vpn.GetStatus() +func (lc *LanternCore) SetSmartRoutingEnabled(enabled bool) error { + return lc.client.EnableSmartRouting(lc.ctx, enabled) } -func (lc *LanternCore) IsVPNRunning() (bool, error) { - st, err := vpn.GetStatus() +func (lc *LanternCore) IsSmartRoutingEnabled() bool { + s, err := lc.client.Settings(lc.ctx) if err != nil { - return false, err + return false + } + v, ok := s[settings.SmartRoutingKey] + if !ok { + return false } - return st.TunnelOpen, nil + b, _ := v.(bool) + return b } func (lc *LanternCore) IsRadianceConnected() bool { - return lc.rad != nil + return lc.client != nil } func (lc *LanternCore) MyDeviceId() string { - return settings.GetString(settings.DeviceIDKey) + s, err := lc.client.Settings(lc.ctx) + if err != nil { + return "" + } + v, _ := s[settings.DeviceIDKey].(string) + return v } func (lc *LanternCore) UpdateLocale(locale string) error { - slog.Debug("Updating locale", "locale", locale) - settings.Set(settings.LocaleKey, locale) - return nil -} - -func (lc *LanternCore) ReferralAttachment(referralCode string) (bool, error) { - return lc.apiClient.ReferralAttach(context.Background(), referralCode) + _, err := lc.client.PatchSettings(lc.ctx, settings.Settings{settings.LocaleKey: locale}) + return err } func (lc *LanternCore) AvailableFeatures() []byte { - features := lc.rad.Features() - slog.Debug("Available features", "features", features) + features, err := lc.client.Features(lc.ctx) + if err != nil { + slog.Error("Error getting features", "error", err) + return nil + } jsonBytes, err := json.Marshal(features) if err != nil { slog.Error("Error marshalling features", "error", err) @@ -396,9 +343,12 @@ func (lc *LanternCore) AvailableFeatures() []byte { } func (lc *LanternCore) GetAvailableServers() []byte { - // Use ServersJSON which marshals under the lock, avoiding GC write barrier - // panics when pointer-rich sing-box types are copied on a CGo callback stack. - jsonBytes, err := lc.rad.ServerManager().ServersJSON() + srvs, err := lc.client.Servers(lc.ctx) + if err != nil { + slog.Error("Error getting servers", "error", err) + return nil + } + jsonBytes, err := json.Marshal(srvs) if err != nil { slog.Error("Error marshalling servers", "error", err) return nil @@ -406,15 +356,78 @@ func (lc *LanternCore) GetAvailableServers() []byte { return jsonBytes } -// LoadInstalledApps fetches the app list or rescans if needed using common macOS locations -// currently only works on/enabled for macOS +func (lc *LanternCore) GetServerByTagJSON(tag string) ([]byte, bool, error) { + server, found, err := lc.client.GetServerByTag(lc.ctx, tag) + if err != nil { + return nil, false, err + } + if !found { + return nil, false, nil + } + jsonBytes, err := json.Marshal(server) + if err != nil { + return nil, false, fmt.Errorf("error marshalling server: %w", err) + } + return jsonBytes, true, nil +} + +///////////////////// +// Background // +///////////////////// + +var listenerManager = &backgroundListenerManager{ + cancel: func() {}, +} + +type backgroundListenerManager struct { + cancel context.CancelFunc + isRunning bool + mu sync.Mutex +} + +func (lc *LanternCore) StartBackgroundListeners() { + slog.Info("Starting background listeners...") + listenerManager.mu.Lock() + defer listenerManager.mu.Unlock() + + if listenerManager.isRunning { + slog.Info("Background listeners already running") + return + } + + _, cancel := context.WithCancel(lc.ctx) + listenerManager.cancel = cancel + listenerManager.isRunning = true + + // Auto-selected and data cap listeners are already running from initialization. + // This method is kept for compatibility but the listeners start automatically. + slog.Info("Background listeners started") +} + +func (lc *LanternCore) StopBackgroundListeners() { + slog.Info("Stopping background listeners...") + listenerManager.mu.Lock() + defer listenerManager.mu.Unlock() + if !listenerManager.isRunning { + slog.Info("Background listeners not running") + return + } + listenerManager.cancel() + listenerManager.isRunning = false + slog.Info("Background listeners stopped") +} + +///////////////// +// Split Tunnel // +///////////////// + +// TODO: ??? not sure what to do about this one. it can't access dataDir func (lc *LanternCore) LoadInstalledApps(dataDir string) (string, error) { appsList := []*apps.AppData{} apps.LoadInstalledApps(dataDir, func(a ...*apps.AppData) error { appsList = append(appsList, a...) return nil }) - b, err := json.Marshal(appsList) if err != nil { return "", err @@ -422,215 +435,248 @@ func (lc *LanternCore) LoadInstalledApps(dataDir string) (string, error) { return string(b), nil } -// SetSplitTunnelingEnabled turns split tunneling on or off for this device func (lc *LanternCore) SetSplitTunnelingEnabled(enabled bool) { - if enabled { - lc.splitTunnel.Enable() - } else { - lc.splitTunnel.Disable() + if err := lc.client.EnableSplitTunneling(lc.ctx, enabled); err != nil { + slog.Error("Error setting split tunneling", "error", err) } } -// IsSplitTunnelingEnabled returns whether split tunneling is currently enabled func (lc *LanternCore) IsSplitTunnelingEnabled() bool { - return lc.splitTunnel.IsEnabled() + s, err := lc.client.Settings(lc.ctx) + if err != nil { + return false + } + v, ok := s[settings.SplitTunnelKey] + if !ok { + return false + } + b, _ := v.(bool) + return b } -// AddSplitTunnelItem adds a single split tunnel rule func (lc *LanternCore) AddSplitTunnelItem(filterType, item string) error { - return lc.splitTunnel.AddItem(filterType, item) + filter := filterFromTypeAndItems(filterType, []string{item}) + return lc.client.AddSplitTunnelItems(lc.ctx, filter) } -// AddSplitTunnelItems adds multiple split tunnel rules from a comma-separated string func (lc *LanternCore) AddSplitTunnelItems(items string) error { split := splitCSVClean(items) + filter := platformFilter(split) + return lc.client.AddSplitTunnelItems(lc.ctx, filter) +} - var vpnFilter vpn.Filter - if common.IsMacOS() { - vpnFilter = vpn.Filter{ - ProcessPathRegex: split, - } - } else if common.IsWindows() { - vpnFilter = vpn.Filter{ - ProcessPath: split, - } - } else { - vpnFilter = vpn.Filter{ - PackageName: split, - } - } - - return lc.splitTunnel.AddItems(vpnFilter) +func (lc *LanternCore) RemoveSplitTunnelItem(filterType, item string) error { + filter := filterFromTypeAndItems(filterType, []string{item}) + return lc.client.RemoveSplitTunnelItems(lc.ctx, filter) } func (lc *LanternCore) RemoveSplitTunnelItems(items string) error { split := splitCSVClean(items) + filter := platformFilter(split) + return lc.client.RemoveSplitTunnelItems(lc.ctx, filter) +} - var vpnFilter vpn.Filter - if common.IsMacOS() { - vpnFilter = vpn.Filter{ - ProcessPathRegex: split, - } - } else if common.IsWindows() { - vpnFilter = vpn.Filter{ - ProcessPath: split, - } - } else { - vpnFilter = vpn.Filter{ - PackageName: split, - } +func (lc *LanternCore) GetSplitTunnelStateJSON() (string, error) { + filter, err := lc.client.SplitTunnelFilters(lc.ctx) + if err != nil { + return "{}", nil + } + b, err := json.Marshal(filter) + if err != nil { + return "{}", nil + } + return string(b), nil +} + +func (lc *LanternCore) GetSplitTunnelItems(filterType string) (string, error) { + filter, err := lc.client.SplitTunnelFilters(lc.ctx) + if err != nil { + return "", err } - return lc.splitTunnel.RemoveItems(vpnFilter) + items := itemsForType(filter, filterType) + b, err := json.Marshal(items) + if err != nil { + return "", err + } + return string(b), nil } -// RemoveSplitTunnelItem removes a single split tunnel rule -func (lc *LanternCore) RemoveSplitTunnelItem(filterType, item string) error { - return lc.splitTunnel.RemoveItem(filterType, item) +func (lc *LanternCore) GetEnabledApps() (string, error) { + filter, err := lc.client.SplitTunnelFilters(lc.ctx) + if err != nil { + return "", err + } + // Return all process-based items as enabled apps + var enabledApps []string + enabledApps = append(enabledApps, filter.ProcessPath...) + enabledApps = append(enabledApps, filter.ProcessPathRegex...) + enabledApps = append(enabledApps, filter.PackageName...) + b, err := json.Marshal(enabledApps) + if err != nil { + return "", err + } + return string(b), nil } -// resolveLogDir returns a directory that contains the logs -func resolveLogDir(logFilePath string) string { - p := strings.TrimSpace(logFilePath) - if p == "" { - return settings.GetString(settings.LogPathKey) +///////////////// +// Issue Report // +///////////////// + +func (lc *LanternCore) ReportIssue(email, issueType, description, device, model, logFilePath string) error { + it := parseIssueType(issueType) + var attachments []string + if logFilePath != "" { + attachments = append(attachments, logFilePath) + } + return lc.client.ReportIssue(lc.ctx, it, description, email, attachments) +} + +func parseIssueType(s string) issue.IssueType { + switch strings.ToLower(s) { + case "cannot_complete_purchase": + return issue.CannotCompletePurchase + case "cannot_sign_in": + return issue.CannotSignIn + case "spinner_loads_endlessly": + return issue.SpinnerLoadsEndlessly + case "cannot_access_blocked_sites": + return issue.CannotAccessBlockedSites + case "slow": + return issue.Slow + case "cannot_link_device": + return issue.CannotLinkDevice + case "application_crashes": + return issue.ApplicationCrashes + case "update_fails": + return issue.UpdateFails + default: + return issue.Other + } +} + +///////////////// +// Account // +///////////////// + +func (lc *LanternCore) DataCapInfo() (string, error) { + info, err := lc.client.DataCapInfo(lc.ctx) + if err != nil { + return "", err } - if st, err := os.Stat(p); err == nil && st.IsDir() { - return p + jsonBytes, err := json.Marshal(info) + if err != nil { + return "", fmt.Errorf("error marshalling DataCapInfo: %w", err) } - return filepath.Dir(p) + return string(jsonBytes), nil } -// ReportIssue is used to send an issue report via Radiance. -// We include a few helpful config files plus the main Lantern + Flutter logs when available -func (lc *LanternCore) ReportIssue( - email, issueType, description, device, model, logFilePath string, -) error { - report := radiance.IssueReport{ - Type: issueType, - Description: description, - Device: device, - Model: model, +func (lc *LanternCore) UserData() ([]byte, error) { + userData, err := lc.client.UserData(lc.ctx) + if err != nil { + return nil, err } + return json.Marshal(userData) +} - // Attach config files from the Lantern data directory - dataDir := settings.GetString(settings.DataPathKey) - configFiles := []string{ - "config.json", - "servers.json", - "split-tunnel.json", +func (lc *LanternCore) FetchUserData() ([]byte, error) { + userData, err := lc.client.FetchUserData(lc.ctx) + if err != nil { + return nil, err } + return json.Marshal(userData) +} - for _, name := range configFiles { - path := filepath.Join(dataDir, name) - b, err := os.ReadFile(path) - if err != nil { - if !os.IsNotExist(err) { - slog.Error("Failed to read file for issue report", - "file", name, - "path", path, - "error", err, - ) - } - continue - } - if len(b) == 0 { - continue - } +func (lc *LanternCore) OAuthLoginUrl(provider string) (string, error) { + return lc.client.OAuthLoginUrl(lc.ctx, provider) +} - report.Attachments = append(report.Attachments, &issue.Attachment{ - Name: name, - Data: b, - }) +func (lc *LanternCore) OAuthLoginCallback(oAuthToken string) ([]byte, error) { + userData, err := lc.client.OAuthLoginCallback(lc.ctx, oAuthToken) + if err != nil { + return nil, err } + return json.Marshal(userData) +} - // On IOS flutter.log file should be attached separately - // since flutter.log is in a different location due to tunnel running in a different process - // On other platforms flutter.log is already included in the main Lantern log file - if logFilePath != "" { - report.Attachments = append( - report.Attachments, - utils.CreateLogAttachment(logFilePath)..., - ) +func (lc *LanternCore) Login(email, password string) ([]byte, error) { + userData, err := lc.client.Login(lc.ctx, email, password) + if err != nil { + return nil, err } + return json.Marshal(userData) +} - // Send issue report via Radiance - if err := lc.rad.ReportIssue(email, report); err != nil { - return fmt.Errorf("error reporting issue: %w", err) +func (lc *LanternCore) SignUp(email, password string) error { + _, _, err := lc.client.SignUp(lc.ctx, email, password) + return err +} + +func (lc *LanternCore) Logout(email string) ([]byte, error) { + userData, err := lc.client.Logout(lc.ctx, email) + if err != nil { + return nil, err } + return json.Marshal(userData) +} - slog.Debug("Reported issue", "type", issueType, "device", device, "model", model) - return nil +func (lc *LanternCore) StartRecoveryByEmail(email string) error { + return lc.client.StartRecoveryByEmail(lc.ctx, email) } -// DataCapInfo returns information about this user's data cap. Only valid for free accounts -func (lc *LanternCore) DataCapInfo() (string, error) { - return lc.apiClient.DataCapInfo(context.Background()) +func (lc *LanternCore) ValidateChangeEmailCode(email, code string) error { + return lc.client.ValidateEmailRecoveryCode(lc.ctx, email, code) } -// DataCapStream starts a stream to receive data cap updates -func (lc *LanternCore) DataCapStream(ctx context.Context) error { - return lc.apiClient.DataCapStream(ctx) +func (lc *LanternCore) CompleteRecoveryByEmail(email, password, code string) error { + return lc.client.CompleteRecoveryByEmail(lc.ctx, email, password, code) } -// User Methods -// UserData returns user data that has already been fetched. -// If user data has not been fetched yet (e.g., for a first-time user), this method will return an error. -// This is expected behavior and not necessarily a problem. -func (lc *LanternCore) UserData() ([]byte, error) { - return lc.apiClient.UserData() +func (lc *LanternCore) DeleteAccount(email, password string, _ bool) ([]byte, error) { + userData, err := lc.client.DeleteAccount(lc.ctx, email, password) + if err != nil { + return nil, err + } + return json.Marshal(userData) } -// FetchUserData will get the user data from the server -func (lc *LanternCore) FetchUserData() ([]byte, error) { - return lc.apiClient.FetchUserData(context.Background()) +func (lc *LanternCore) RemoveDevice(deviceID string) (*account.LinkResponse, error) { + return lc.client.RemoveDevice(lc.ctx, deviceID) } -// OAuth Methods -func (lc *LanternCore) OAuthLoginUrl(provider string) (string, error) { - return lc.apiClient.OAuthLoginUrl(context.Background(), provider) +func (lc *LanternCore) StartChangeEmail(newEmail, password string) error { + return lc.client.StartChangeEmail(lc.ctx, newEmail, password) } -func (lc *LanternCore) OAuthLoginCallback(oAuthToken string) ([]byte, error) { - return lc.apiClient.OAuthLoginCallback(context.Background(), oAuthToken) +func (lc *LanternCore) CompleteChangeEmail(email, password, code string) error { + return lc.client.CompleteChangeEmail(lc.ctx, email, password, code) } -func (lc *LanternCore) StripeSubscriptionPaymentRedirect(subscriptionType, planID, email string) (string, error) { - redirectBody := api.PaymentRedirectData{ - Provider: "stripe", - Plan: planID, - DeviceName: settings.GetString(settings.DeviceIDKey), - Email: email, - BillingType: api.SubscriptionType(subscriptionType), - } - return lc.SubscriptionPaymentRedirectURL(redirectBody) +func (lc *LanternCore) ReferralAttachment(referralCode string) (bool, error) { + return lc.client.ReferralAttach(lc.ctx, referralCode) } +///////////////// +// Payments // +///////////////// + func (lc *LanternCore) StripeSubscription(email, planID string) (string, error) { - slog.Debug("Creating stripe subscription") - return lc.apiClient.NewStripeSubscription(context.Background(), email, planID) + return lc.client.NewStripeSubscription(lc.ctx, email, planID) } func (lc *LanternCore) Plans(channel string) (string, error) { - slog.Debug("Getting plans") - return lc.apiClient.SubscriptionPlans(context.Background(), channel) + return lc.client.SubscriptionPlans(lc.ctx, channel) } + func (lc *LanternCore) StripeBillingPortalUrl() (string, error) { - slog.Debug("Getting stripe billing portal") - return lc.apiClient.StripeBillingPortalUrl(context.Background()) + return lc.client.StripeBillingPortalURL(lc.ctx) } func (lc *LanternCore) AcknowledgeGooglePurchase(purchaseToken, planId string) (string, error) { - slog.Debug("Purchase token: ", "token", purchaseToken, "planId", planId) params := map[string]string{ "purchaseToken": purchaseToken, "planId": planId, } - status, err := lc.apiClient.VerifySubscription(context.Background(), api.GoogleService, params) - if err != nil { - return "", fmt.Errorf("error acknowledging google purchase: %w", err) - } - slog.Debug("acknowledge google purchase:", "status", status) - return status, nil + return lc.client.VerifySubscription(lc.ctx, account.GoogleService, params) } func (lc *LanternCore) AcknowledgeApplePurchase(receipt, planII string) (string, error) { @@ -638,132 +684,68 @@ func (lc *LanternCore) AcknowledgeApplePurchase(receipt, planII string) (string, "receipt": receipt, "planId": planII, } - data, err := lc.apiClient.VerifySubscription(context.Background(), api.AppleService, params) - if err != nil { - return "", fmt.Errorf("error acknowledging apple purchase: %w", err) - } - slog.Debug("acknowledge apple purchase: ", "data", data) - return data, nil + return lc.client.VerifySubscription(lc.ctx, account.AppleService, params) } -func (lc *LanternCore) SubscriptionPaymentRedirectURL(redirectBody api.PaymentRedirectData) (string, error) { - slog.Debug("Getting payment redirect URL") - return lc.apiClient.SubscriptionPaymentRedirectURL(context.Background(), redirectBody) +func (lc *LanternCore) SubscriptionPaymentRedirectURL(redirectBody account.PaymentRedirectData) (string, error) { + return lc.client.SubscriptionPaymentRedirectURL(lc.ctx, redirectBody) +} + +func (lc *LanternCore) StripeSubscriptionPaymentRedirect(subscriptionType, planID, email string) (string, error) { + deviceID := lc.MyDeviceId() + redirectBody := account.PaymentRedirectData{ + Provider: "stripe", + Plan: planID, + DeviceName: deviceID, + Email: email, + BillingType: account.SubscriptionType(subscriptionType), + } + return lc.SubscriptionPaymentRedirectURL(redirectBody) } func (lc *LanternCore) PaymentRedirect(provider, planId, email string) (string, error) { - slog.Debug("Payment redirect") - deviceName := settings.GetString(settings.DeviceIDKey) - body := api.PaymentRedirectData{ + deviceName := lc.MyDeviceId() + body := account.PaymentRedirectData{ Provider: provider, Plan: planId, DeviceName: deviceName, Email: email, } - paymentRedirect, err := lc.apiClient.PaymentRedirect(context.Background(), body) - if err != nil { - return "", fmt.Errorf("error getting payment redirect: %w", err) - } - slog.Debug("Payment redirect response: ", "response", paymentRedirect) - return paymentRedirect, nil -} - -/// User management apis - -func (lc *LanternCore) Login(email, password string) ([]byte, error) { - slog.Debug("Logging in user") - return lc.apiClient.Login(context.Background(), email, password) -} - -func (lc *LanternCore) SignUp(email, password string) error { - slog.Debug("Signing up user") - salt, body, err := lc.apiClient.SignUp(context.Background(), email, password) - if err != nil { - return fmt.Errorf("error signing up: %w", err) - } - slog.Debug("SignUp response: ", "salt", salt, "body", body) - return nil -} - -func (lc *LanternCore) Logout(email string) ([]byte, error) { - slog.Debug("Logging out") - return lc.apiClient.Logout(context.Background(), email) -} - -// Email Recovery Methods -// This will start the email recovery process by sending a recovery code to the user's email -func (lc *LanternCore) StartRecoveryByEmail(email string) error { - slog.Debug("Starting change email") - return lc.apiClient.StartRecoveryByEmail(context.Background(), email) -} - -// This will validate the recovery code sent to the user's email -func (lc *LanternCore) ValidateChangeEmailCode(email, code string) error { - slog.Debug("Validating change email code") - return lc.apiClient.ValidateEmailRecoveryCode(context.Background(), email, code) -} - -// This will complete the email recovery by setting the new password -func (lc *LanternCore) CompleteRecoveryByEmail(email, password, code string) error { - slog.Debug("Completing email recovery") - return lc.apiClient.CompleteRecoveryByEmail(context.Background(), email, password, code) -} - -func (lc *LanternCore) DeleteAccount(email, password string, isOAuthUser bool) ([]byte, error) { - slog.Debug("Deleting account") - return lc.apiClient.DeleteAccount(context.Background(), email, password, isOAuthUser) -} - -func (lc *LanternCore) RemoveDevice(deviceID string) (*api.LinkResponse, error) { - slog.Debug("Removing device: ", "deviceID", deviceID) - return lc.apiClient.RemoveDevice(context.Background(), deviceID) -} - -// Change email -func (lc *LanternCore) StartChangeEmail(newEmail, password string) error { - slog.Debug("Starting change email") - return lc.apiClient.StartChangeEmail(context.Background(), newEmail, password) -} - -func (lc *LanternCore) CompleteChangeEmail(email, password, code string) error { - slog.Debug("Completing change email") - return lc.apiClient.CompleteChangeEmail(context.Background(), email, password, code) + return lc.client.PaymentRedirect(lc.ctx, body) } func (lc *LanternCore) ActivationCode(email, resellerCode string) error { - slog.Debug("Getting activation code") - purchase, err := lc.apiClient.ActivationCode(context.Background(), email, resellerCode) + purchase, err := lc.client.ActivationCode(lc.ctx, email, resellerCode) if err != nil { return fmt.Errorf("error getting activation code: %w", err) } - slog.Debug("ActivationCode response: ", "response", purchase) if purchase.Status != "ok" { return fmt.Errorf("activation code failed: %s", purchase.Status) } return nil } +///////////////////// +// Private Servers // +///////////////////// + func (lc *LanternCore) DigitalOceanPrivateServer(events utils.PrivateServerEventListener) error { - slog.Debug("Starting DigitalOcean private server flow") - return privateserver.StartDigitalOceanPrivateServerFlow(events, lc.serverManager) + return privateserver.StartDigitalOceanPrivateServerFlow(events, lc.client) } func (lc *LanternCore) GoogleCloudPrivateServer(events utils.PrivateServerEventListener) error { - return privateserver.StartGoogleCloudPrivateServerFlow(events, lc.serverManager) + return privateserver.StartGoogleCloudPrivateServerFlow(events, lc.client) } func (lc *LanternCore) ValidateSession() error { - slog.Debug("Validating private server session") return privateserver.ValidateSession(context.Background()) } func (lc *LanternCore) SelectAccount(account string) error { - slog.Debug("Selecting account: ", "account", account) return privateserver.SelectAccount(account) } func (lc *LanternCore) SelectProject(project string) error { - slog.Debug("Selecting project: ", "project", project) return privateserver.SelectProject(project) } @@ -776,19 +758,15 @@ func (lc *LanternCore) CancelDeployment() error { } func (lc *LanternCore) AddServerManagerInstance(ip, port, accessToken, tag string, events utils.PrivateServerEventListener) error { - return privateserver.AddServerManually(ip, port, accessToken, tag, lc.serverManager, events) + return privateserver.AddServerManually(ip, port, accessToken, tag, lc.client, events) } + func (lc *LanternCore) InviteToServerManagerInstance(ip, port, accessToken, inviteName string) (string, error) { portInt, err := parsePort(port) if err != nil { return "", err } - accessToken, err = lc.serverManager.InviteToPrivateServer(ip, portInt, accessToken, inviteName) - if err != nil { - return "", fmt.Errorf("error inviting to server manager instance: %w", err) - } - slog.Debug("Invite to server manager instance:", "ip", ip, "port", portInt, "name", inviteName) - return accessToken, nil + return lc.client.InviteToPrivateServer(lc.ctx, ip, portInt, accessToken, inviteName) } func (lc *LanternCore) RevokeServerManagerInvite(ip, port, accessToken, inviteName string) error { @@ -796,13 +774,11 @@ func (lc *LanternCore) RevokeServerManagerInvite(ip, port, accessToken, inviteNa if err != nil { return err } - slog.Debug("Revoking invite:", "name", inviteName, "ip", ip, "port", port) - return lc.serverManager.RevokePrivateServerInvite(ip, portInt, accessToken, inviteName) + return lc.client.RevokePrivateServerInvite(lc.ctx, ip, portInt, accessToken, inviteName) } func (lc *LanternCore) DeleteServer(tag string) error { - slog.Debug("Deleting server with tag: ", "tag", tag) - return lc.serverManager.RemoveServer(tag) + return lc.client.RemoveServers(lc.ctx, []string{tag}) } func (lc *LanternCore) UpdatePrivateServerName(oldTag, newTag string) error { @@ -813,8 +789,18 @@ func (lc *LanternCore) UpdatePrivateServerName(oldTag, newTag string) error { return nil } - // Ensure the source exists in user servers. - userServers := lc.serverManager.Servers()[servers.SGUser] + // Get current servers to find the one being renamed + srvs, err := lc.client.Servers(lc.ctx) + if err != nil { + return fmt.Errorf("failed to get servers: %w", err) + } + + // Check source exists in user servers + userServers, ok := srvs[servers.SGUser] + if !ok { + return fmt.Errorf("no user servers found") + } + sourceExists := false for _, ep := range userServers.Endpoints { if ep.Tag == oldTag { @@ -834,11 +820,13 @@ func (lc *LanternCore) UpdatePrivateServerName(oldTag, newTag string) error { return fmt.Errorf("server with tag %q not found", oldTag) } - // Prevent collisions against any existing server tag. - if _, exists := lc.serverManager.GetServerByTag(newTag); exists { + // Check new tag doesn't collide + _, exists, _ := lc.client.GetServerByTag(lc.ctx, newTag) + if exists { return fmt.Errorf("server with tag %q already exists", newTag) } + // Rename by updating the options for i, ep := range userServers.Endpoints { if ep.Tag == oldTag { userServers.Endpoints[i].Tag = newTag @@ -853,14 +841,32 @@ func (lc *LanternCore) UpdatePrivateServerName(oldTag, newTag string) error { delete(userServers.Locations, oldTag) userServers.Locations[newTag] = loc } - if err := lc.serverManager.SetServers(servers.SGUser, userServers); err != nil { - return fmt.Errorf("failed to rename private server %q to %q: %w", oldTag, newTag, err) + + // Remove old, add new + if err := lc.client.RemoveServers(lc.ctx, []string{oldTag}); err != nil { + return fmt.Errorf("failed to remove old server %q: %w", oldTag, err) + } + if err := lc.client.AddServers(lc.ctx, servers.SGUser, userServers); err != nil { + return fmt.Errorf("failed to add renamed server %q: %w", newTag, err) } return nil } +func (lc *LanternCore) AddServerBasedOnURLs(urls string, skipCertVerification bool, _ string) error { + urlList := strings.Split(urls, ",") + for i, u := range urlList { + urlList[i] = strings.TrimSpace(u) + } + return lc.client.AddServersByURL(lc.ctx, urlList, skipCertVerification) +} + +///////////////// +// Helpers // +///////////////// + func parsePort(port string) (int, error) { - portInt, err := strconv.Atoi(port) + portInt := 0 + _, err := fmt.Sscanf(port, "%d", &portInt) if err != nil { return 0, fmt.Errorf("invalid port %q: %w", port, err) } @@ -870,29 +876,6 @@ func parsePort(port string) (int, error) { return portInt, nil } -func (lc *LanternCore) SetBlockAdsEnabled(enabled bool) error { - return vpn.SetAdBlock(enabled) -} - -func (lc *LanternCore) IsBlockAdsEnabled() bool { - return vpn.AdBlockEnabled() -} - -func (lc *LanternCore) SetSmartRoutingEnabled(enabled bool) error { - return vpn.SetSmartRouting(enabled) -} - -func (lc *LanternCore) IsSmartRoutingEnabled() bool { - return vpn.SmartRoutingEnabled() -} - -func (lc *LanternCore) AddServerBasedOnURLs(urls string, skipCertVerification bool, serverName string) error { - slog.Debug("Adding server based on URLs", "urls", urls, "skipCertVerification", skipCertVerification) - return lc.serverManager.AddServerBasedOnURLs(context.Background(), urls, skipCertVerification, serverName) -} - -// splitCSVClean splits a comma-separated string into a stable list -// It trims whitespace and surrounding quotes and removes duplicates func splitCSVClean(s string) []string { raw := strings.Split(s, ",") out := make([]string, 0, len(raw)) @@ -915,82 +898,57 @@ func splitCSVClean(s string) []string { return out } -func (lc *LanternCore) GetSplitTunnelStateJSON() (string, error) { - path := filepath.Join(settings.GetString(settings.DataPathKey), "split-tunnel.json") - b, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return `{}`, nil - } - return "", err - } - if len(b) == 0 { - return `{}`, nil - } - return string(b), nil -} - -func (lc *LanternCore) splitTunnelHandler() (*vpn.SplitTunnel, error) { - if lc.splitTunnel != nil { - return lc.splitTunnel, nil - } - st, err := vpn.NewSplitTunnelHandler() - if err != nil { - return nil, err - } - lc.splitTunnel = st - return st, nil -} - -func (lc *LanternCore) GetSplitTunnelItems(filterType string) (string, error) { - st, err := lc.splitTunnelHandler() - if err != nil { - return "", err - } - return st.ItemsJSON(filterType) -} - -func jsonNumberToIntString(f float64) string { - // ports are integral; safe enough here - return string([]byte((func() string { - n := int(f) - return itoa(n) - })())) -} - -// tiny local itoa to avoid importing strconv in this file (optional) -func itoa(n int) string { - if n == 0 { - return "0" - } - neg := n < 0 - if neg { - n = -n - } - buf := make([]byte, 0, 12) - for n > 0 { - d := n % 10 - buf = append(buf, byte('0'+d)) - n /= 10 - } - if neg { - buf = append(buf, '-') - } - // reverse - for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 { - buf[i], buf[j] = buf[j], buf[i] - } - return string(buf) -} - -func (lc *LanternCore) GetAppDataDir() string { - return settings.GetString(settings.DataPathKey) -} - -func (lc *LanternCore) GetEnabledApps() (string, error) { - st, err := lc.splitTunnelHandler() - if err != nil { - return "", err +func platformFilter(items []string) vpn.SplitTunnelFilter { + if common.IsMacOS() { + return vpn.SplitTunnelFilter{ProcessPathRegex: items} + } else if common.IsWindows() { + return vpn.SplitTunnelFilter{ProcessPath: items} + } + return vpn.SplitTunnelFilter{PackageName: items} +} + +func filterFromTypeAndItems(filterType string, items []string) vpn.SplitTunnelFilter { + switch filterType { + case vpn.TypeDomain: + return vpn.SplitTunnelFilter{Domain: items} + case vpn.TypeDomainSuffix: + return vpn.SplitTunnelFilter{DomainSuffix: items} + case vpn.TypeDomainKeyword: + return vpn.SplitTunnelFilter{DomainKeyword: items} + case vpn.TypeDomainRegex: + return vpn.SplitTunnelFilter{DomainRegex: items} + case vpn.TypeProcessName: + return vpn.SplitTunnelFilter{ProcessName: items} + case vpn.TypeProcessPath: + return vpn.SplitTunnelFilter{ProcessPath: items} + case vpn.TypeProcessPathRegex: + return vpn.SplitTunnelFilter{ProcessPathRegex: items} + case vpn.TypePackageName: + return vpn.SplitTunnelFilter{PackageName: items} + default: + return vpn.SplitTunnelFilter{} + } +} + +func itemsForType(filter vpn.SplitTunnelFilter, filterType string) []string { + switch filterType { + case vpn.TypeDomain: + return filter.Domain + case vpn.TypeDomainSuffix: + return filter.DomainSuffix + case vpn.TypeDomainKeyword: + return filter.DomainKeyword + case vpn.TypeDomainRegex: + return filter.DomainRegex + case vpn.TypeProcessName: + return filter.ProcessName + case vpn.TypeProcessPath: + return filter.ProcessPath + case vpn.TypeProcessPathRegex: + return filter.ProcessPathRegex + case vpn.TypePackageName: + return filter.PackageName + default: + return nil } - return st.EnabledAppsJSON() } diff --git a/lantern-core/ffi/ffi.go b/lantern-core/ffi/ffi.go index 80b136ab57..b079e7134e 100644 --- a/lantern-core/ffi/ffi.go +++ b/lantern-core/ffi/ffi.go @@ -273,20 +273,15 @@ func getAutoLocation() *C.char { if errStr != nil { return errStr } - location, err := vpn_tunnel.GetAutoLocation() + server, err := vpn_tunnel.GetAutoLocation(c.Client()) if err != nil { return SendError(err) } - // Use GetServerByTagJSON which marshals internally, avoiding GC write - // barrier panics when pointer-rich Server types are copied on the CGo stack. - jsonBytes, ok, err := c.GetServerByTagJSON(location.Lantern) + jsonBytes, err := json.Marshal(server) if err != nil { return SendError(fmt.Errorf("error marshalling server: %v", err)) } - if !ok { - return SendError(fmt.Errorf("error finding server with tag: %s", location.Lantern)) - } return C.CString(string(jsonBytes)) } @@ -1041,15 +1036,6 @@ func updatePrivateServerName(_oldName, _newName *C.char) *C.char { return C.CString("ok") } -//export getAppDataDir -func getAppDataDir() *C.char { - c, errStr := requireCore() - if errStr != nil { - return errStr - } - return C.CString(c.GetAppDataDir()) -} - //export getEnabledApps func getEnabledApps() *C.char { c, errStr := requireCore() diff --git a/lantern-core/ffi/ffi_linux.go b/lantern-core/ffi/ffi_linux.go index 9c3089270b..e3d30b60cc 100644 --- a/lantern-core/ffi/ffi_linux.go +++ b/lantern-core/ffi/ffi_linux.go @@ -18,17 +18,16 @@ import ( "sync" "time" - "github.com/getlantern/radiance/servers" + "github.com/getlantern/radiance/ipc" "github.com/getlantern/radiance/vpn" - "github.com/getlantern/radiance/vpn/ipc" ) const ( linuxServiceName = "lanternd" - linuxSocketPath = "/var/run/lantern/lanternd.sock" ) var ( + ipcClient = ipc.NewClient() linuxStatusOnce sync.Once linuxLastStatusMu sync.Mutex linuxLastStatus string @@ -38,15 +37,14 @@ func requireLanternServiceAvailable() error { ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) defer cancel() - st, err := ipc.GetStatus(ctx) - if err == nil && st != "" { + if _, err := ipcClient.VPNStatus(ctx); err == nil { return nil } if diag := systemdDiag(linuxServiceName); diag != "" { - return fmt.Errorf("%s not reachable (%s): %s", linuxServiceName, linuxSocketPath, diag) + return fmt.Errorf("%s not reachable: %s", linuxServiceName, diag) } - return fmt.Errorf("%s not reachable (%s)", linuxServiceName, linuxSocketPath) + return fmt.Errorf("%s not reachable", linuxServiceName) } func systemdDiag(unit string) string { @@ -79,7 +77,7 @@ func systemdDiag(unit string) string { } } -func startLinuxStatusPoller() { +func startLinuxStatusListener() { linuxStatusOnce.Do(func() { go func() { t := time.NewTicker(500 * time.Millisecond) @@ -89,62 +87,50 @@ func startLinuxStatusPoller() { if statusPort == 0 { continue } - - ctx, cancel := context.WithTimeout(context.Background(), 400*time.Millisecond) - st, err := ipc.GetStatus(ctx) - cancel() - - ui := mapIPCStateToUIStatus(st, err) - - linuxLastStatusMu.Lock() - changed := ui != linuxLastStatus - if changed { - linuxLastStatus = ui - } - linuxLastStatusMu.Unlock() - - if changed { - sendStatusToPort(VPNStatus(ui)) - } + ipcClient.VPNStatusEvents(context.Background(), func(status vpn.StatusUpdateEvent) { + var err error + if status.Error != "" { + err = errors.New(status.Error) + } + ui := mapVPNStatusToUI(status.Status, err) + + linuxLastStatusMu.Lock() + changed := ui != linuxLastStatus + if changed { + linuxLastStatus = ui + } + linuxLastStatusMu.Unlock() + + if changed { + sendStatusToPort(VPNStatus(ui)) + } + }) } }() }) } -func mapIPCStateToUIStatus(state ipc.VPNStatus, err error) string { +func mapVPNStatusToUI(status vpn.VPNStatus, err error) string { if err != nil { return string(Disconnected) } - switch state { - case ipc.Connected: + switch status { + case vpn.Connected: return string(Connected) - case ipc.Connecting: + case vpn.Connecting: return string(Connecting) - case ipc.Disconnecting: + case vpn.Disconnecting: return string(Disconnecting) - case ipc.Disconnected: + case vpn.Disconnected: return string(Disconnected) default: return string(Disconnected) } } -func normalizeIPCGroup(locationType string) string { - switch locationType { - case "", "auto", "auto-all": - return "all" - case "privateServer": - return string(servers.SGUser) - case "lanternLocation": - return string(servers.SGLantern) - default: - return locationType - } -} - //export startVPN func startVPN(_logDir, _dataDir, _locale *C.char) *C.char { - startLinuxStatusPoller() + startLinuxStatusListener() sendStatusToPort(Connecting) if err := requireLanternServiceAvailable(); err != nil { @@ -152,7 +138,9 @@ func startVPN(_logDir, _dataDir, _locale *C.char) *C.char { return C.CString(err.Error()) } - if err := vpn.AutoConnect(""); err != nil && !errors.Is(err, ipc.ErrServiceIsNotReady) { + ctx := context.Background() + if err := ipcClient.ConnectVPN(ctx, ""); err != nil && + !errors.Is(err, ipc.ErrServiceIsNotReady) { sendStatusToPort(Error) if errors.Is(err, ipc.ErrIPCNotRunning) { if diagErr := requireLanternServiceAvailable(); diagErr != nil { @@ -173,7 +161,7 @@ func stopVPN() *C.char { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - if err := ipc.StopService(ctx); err != nil { + if err := ipcClient.DisconnectVPN(ctx); err != nil { sendStatusToPort(Disconnected) return C.CString(fmt.Sprintf("stop service failed: %v", err)) } @@ -184,17 +172,16 @@ func stopVPN() *C.char { //export connectToServer func connectToServer(_location, _tag, _logDir, _dataDir, _locale *C.char) *C.char { - locationType := C.GoString(_location) - tag := C.GoString(_tag) - group := normalizeIPCGroup(locationType) - - startLinuxStatusPoller() + startLinuxStatusListener() if err := requireLanternServiceAvailable(); err != nil { return SendError(err) } - if err := vpn.Connect(group, tag); err != nil && !errors.Is(err, ipc.ErrServiceIsNotReady) { + tag := C.GoString(_tag) + ctx := context.Background() + if err := ipcClient.ConnectVPN(ctx, tag); err != nil && + !errors.Is(err, ipc.ErrServiceIsNotReady) { if errors.Is(err, ipc.ErrIPCNotRunning) { if diagErr := requireLanternServiceAvailable(); diagErr != nil { return SendError(diagErr) @@ -211,8 +198,8 @@ func isVPNConnected() C.int { ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond) defer cancel() - st, err := ipc.GetStatus(ctx) - ui := mapIPCStateToUIStatus(st, err) + status, err := ipcClient.VPNStatus(ctx) + ui := mapVPNStatusToUI(status, err) sendStatusToPort(VPNStatus(ui)) diff --git a/lantern-core/ffi/ffi_nonlinux.go b/lantern-core/ffi/ffi_nonlinux.go index 22beb24c9b..33af3fd7d9 100644 --- a/lantern-core/ffi/ffi_nonlinux.go +++ b/lantern-core/ffi/ffi_nonlinux.go @@ -12,21 +12,20 @@ import ( "fmt" "log/slog" - "github.com/getlantern/lantern/lantern-core/utils" "github.com/getlantern/lantern/lantern-core/vpn_tunnel" ) //export startVPN func startVPN(_logDir, _dataDir, _locale *C.char) *C.char { + c, errStr := requireCore() + if errStr != nil { + return errStr + } slog.Debug("startVPN called (non-linux)") sendStatusToPort(Connecting) - if err := vpn_tunnel.StartVPN(nil, &utils.Opts{ - DataDir: C.GoString(_dataDir), - Locale: C.GoString(_locale), - }); err != nil { - err = fmt.Errorf("unable to start vpn server: %v", err) + if err := vpn_tunnel.StartVPN(c.Client()); err != nil { sendStatusToPort(Disconnected) - return C.CString(err.Error()) + return C.CString(fmt.Sprintf("unable to start vpn server: %v", err)) } sendStatusToPort(Connected) return C.CString("ok") @@ -34,12 +33,15 @@ func startVPN(_logDir, _dataDir, _locale *C.char) *C.char { //export stopVPN func stopVPN() *C.char { + c, errStr := requireCore() + if errStr != nil { + return errStr + } slog.Debug("stopVPN called (non-linux)") sendStatusToPort(Disconnecting) - if err := vpn_tunnel.StopVPN(); err != nil { - err = fmt.Errorf("unable to stop vpn server: %v", err) + if err := vpn_tunnel.StopVPN(c.Client()); err != nil { sendStatusToPort(Connected) - return C.CString(err.Error()) + return C.CString(fmt.Sprintf("unable to stop vpn server: %v", err)) } sendStatusToPort(Disconnected) return C.CString("ok") @@ -47,13 +49,14 @@ func stopVPN() *C.char { //export connectToServer func connectToServer(_location, _tag, _logDir, _dataDir, _locale *C.char) *C.char { + c, errStr := requireCore() + if errStr != nil { + return errStr + } locationType := C.GoString(_location) tag := C.GoString(_tag) - if err := vpn_tunnel.ConnectToServer(locationType, tag, nil, &utils.Opts{ - DataDir: C.GoString(_dataDir), - Locale: C.GoString(_locale), - }); err != nil { + if err := vpn_tunnel.ConnectToServer(c.Client(), locationType, tag); err != nil { return SendError(fmt.Errorf("error setting private server: %v", err)) } slog.Debug("connectToServer OK (non-linux)", "tag", tag) @@ -62,7 +65,11 @@ func connectToServer(_location, _tag, _logDir, _dataDir, _locale *C.char) *C.cha //export isVPNConnected func isVPNConnected() C.int { - connected := vpn_tunnel.IsVPNRunning() + c, errStr := requireCore() + if errStr != nil { + return 0 + } + connected := vpn_tunnel.IsVPNRunning(c.Client()) if connected { sendStatusToPort(Connected) return 1 diff --git a/lantern-core/init_desktop.go b/lantern-core/init_desktop.go new file mode 100644 index 0000000000..63bd4d5c79 --- /dev/null +++ b/lantern-core/init_desktop.go @@ -0,0 +1,15 @@ +//go:build !android && !ios && !darwin + +package lanterncore + +import ( + "context" + + "github.com/getlantern/radiance/ipc" + + "github.com/getlantern/lantern/lantern-core/utils" +) + +func createClient(_ context.Context, _ *utils.Opts) (*ipc.Client, error) { + return ipc.NewClient(), nil +} diff --git a/lantern-core/init_mobile.go b/lantern-core/init_mobile.go new file mode 100644 index 0000000000..8540fdb4b0 --- /dev/null +++ b/lantern-core/init_mobile.go @@ -0,0 +1,24 @@ +//go:build android || ios || darwin + +package lanterncore + +import ( + "context" + + "github.com/getlantern/radiance/backend" + "github.com/getlantern/radiance/ipc" + + "github.com/getlantern/lantern/lantern-core/utils" +) + +func createClient(ctx context.Context, opts *utils.Opts) (*ipc.Client, error) { + backendOpts := backend.Options{ + DataDir: opts.DataDir, + LogDir: opts.LogDir, + DeviceID: opts.Deviceid, + LogLevel: opts.LogLevel, + Locale: opts.Locale, + TelemetryConsent: opts.TelemetryConsent, + } + return ipc.NewClient(ctx, backendOpts) +} diff --git a/lantern-core/mobile/mobile.go b/lantern-core/mobile/mobile.go index e5391a57a4..4b82e9d6e8 100644 --- a/lantern-core/mobile/mobile.go +++ b/lantern-core/mobile/mobile.go @@ -1,17 +1,21 @@ package mobile import ( + "context" "encoding/json" "errors" "fmt" "log/slog" + "sync" "sync/atomic" _ "golang.org/x/mobile/bind" - "github.com/getlantern/radiance/api" + "github.com/getlantern/radiance/account" + "github.com/getlantern/radiance/backend" "github.com/getlantern/radiance/common" "github.com/getlantern/radiance/common/settings" + "github.com/getlantern/radiance/ipc" lanterncore "github.com/getlantern/lantern/lantern-core" "github.com/getlantern/lantern/lantern-core/utils" @@ -21,6 +25,10 @@ import ( var ( lanternCore atomic.Value errLanternNotReady = errors.New("radiance not initialized") + + ipcServer *ipc.Server + ipcMu sync.Mutex + ipcOnce sync.Once ) func getCore() (lanterncore.Core, error) { @@ -141,43 +149,68 @@ func IsRadianceConnected() bool { func StartVPN(platform utils.PlatformInterface, opts *utils.Opts) error { slog.Info("Starting VPN") - err := vpn_tunnel.StartVPN(platform, opts) - if err != nil { - return err - } - // On non-iOS/macOS platforms, start the auto location listener - // For iOS/macOS, the listener is managed by Native code due to platform restrictions - - if !common.IsMacOS() && !common.IsIOS() { - slog.Info("Starting auto location listener on non-iOS/macOS platform") - return withCore(func(c lanterncore.Core) error { + return withCore(func(c lanterncore.Core) error { + err := vpn_tunnel.StartVPN(c.Client()) + if err != nil { + return err + } + // On non-iOS/macOS platforms, start the auto location listener + // For iOS/macOS, the listener is managed by Native code due to platform restrictions + if !common.IsMacOS() && !common.IsIOS() { + slog.Info("Starting auto location listener on non-iOS/macOS platform") c.StartBackgroundListeners() - return nil - }) - } - return nil + } + return nil + }) } func StopVPN() error { slog.Info("Stopping VPN") - err := vpn_tunnel.StopVPN() - if err != nil { - return err - } - // On non-iOS/macOS platforms, start the auto location listener since radiance is still running - // For iOS/macOS, the listener is managed by Native code due to platform restrictions - if !common.IsMacOS() && !common.IsIOS() { - slog.Info("Stopping auto location listener on non-iOS/macOS platform") - return withCore(func(c lanterncore.Core) error { + return withCore(func(c lanterncore.Core) error { + err := vpn_tunnel.StopVPN(c.Client()) + if err != nil { + return err + } + // On non-iOS/macOS platforms, stop the auto location listener since radiance is still running + // For iOS/macOS, the listener is managed by Native code due to platform restrictions + if !common.IsMacOS() && !common.IsIOS() { + slog.Info("Stopping auto location listener on non-iOS/macOS platform") c.StopBackgroundListeners() - return nil - }) + } + return nil + }) +} + +func StartIPCServer(platform utils.PlatformInterface, opts *utils.Opts) error { + ipcMu.Lock() + defer ipcMu.Unlock() + if ipcServer != nil { + return nil } - return nil + bopts := backend.Options{ + DataDir: opts.DataDir, + LogDir: opts.LogDir, + Locale: opts.Locale, + LogLevel: opts.LogLevel, + DeviceID: opts.Deviceid, + TelemetryConsent: opts.TelemetryConsent, + PlatformInterface: platform, + } + be, err := backend.NewLocalBackend(context.Background(), bopts) + if err != nil { + return fmt.Errorf("error creating backend for IPC server: %v", err) + } + ipcServer = ipc.NewServer(be, !common.IsMobile()) + return ipcServer.Start() } -func CloseIPC() error { - return vpn_tunnel.CloseIPC() +func CloseIPCServer() error { + ipcMu.Lock() + defer ipcMu.Unlock() + if ipcServer != nil { + ipcServer.Close() + } + return nil } // IsTagAvailable checks if a server with the given tag exists in the server list. @@ -198,20 +231,19 @@ func IsTagAvailable(tag string) bool { // ConnectToServer connects to a server using the provided location type and tag. // It works with private servers and lantern location servers. func ConnectToServer(locationType, tag string, platIfce utils.PlatformInterface, options *utils.Opts) error { - err := vpn_tunnel.ConnectToServer(locationType, tag, platIfce, options) - if err != nil { - return err - } - // On non-iOS/macOS platforms, start the auto location listener since radiance is still running - // For iOS/macOS, the listener is managed by Native code due to platform restrictions - if !common.IsMacOS() && !common.IsIOS() { - slog.Info("Stopping auto location listener on non-iOS/macOS platform") - return withCore(func(c lanterncore.Core) error { + return withCore(func(c lanterncore.Core) error { + err := vpn_tunnel.ConnectToServer(c.Client(), locationType, tag) + if err != nil { + return err + } + // On non-iOS/macOS platforms, stop the auto location listener since radiance is still running + // For iOS/macOS, the listener is managed by Native code due to platform restrictions + if !common.IsMacOS() && !common.IsIOS() { + slog.Info("Stopping auto location listener on non-iOS/macOS platform") c.StopBackgroundListeners() - return nil - }) - } - return nil + } + return nil + }) } // StartAutoLocationListener starts the auto location listener in the core. @@ -238,25 +270,34 @@ func GetAvailableServers() ([]byte, error) { } func IsVPNConnected() bool { - return vpn_tunnel.IsVPNRunning() + ok, err := withCoreR(func(c lanterncore.Core) (bool, error) { + return vpn_tunnel.IsVPNRunning(c.Client()), nil + }) + if err != nil { + return false + } + return ok } func GetSelectedServer() string { - return vpn_tunnel.GetSelectedServer() + s, err := withCoreR(func(c lanterncore.Core) (string, error) { + return vpn_tunnel.GetSelectedServer(c.Client()), nil + }) + if err != nil { + return "" + } + return s } func GetAutoLocation() (string, error) { - location, err := vpn_tunnel.GetAutoLocation() - if err != nil { - return "", err - } return withCoreR(func(c lanterncore.Core) (string, error) { - jsonBytes, ok, err := c.GetServerByTagJSON(location.Lantern) + server, err := vpn_tunnel.GetAutoLocation(c.Client()) if err != nil { - return "", fmt.Errorf("error marshalling server: %v", err) + return "", err } - if !ok { - return "", fmt.Errorf("no server found with tag: %s", location.Lantern) + jsonBytes, err := json.Marshal(server) + if err != nil { + return "", fmt.Errorf("error marshalling server: %v", err) } slog.Debug("Auto location server:", "server", string(jsonBytes)) return string(jsonBytes), nil @@ -343,17 +384,23 @@ func AcknowledgeGooglePurchase(purchaseToken, planId string) ([]byte, error) { if err != nil { return nil, err } - var resp api.VerifySubscriptionResponse + var resp account.VerifySubscriptionResponse if err := json.Unmarshal([]byte(data), &resp); err != nil { return nil, fmt.Errorf("error unmarshalling acknowledge google purchase response: %v", err) } - if resp.ActualUserId != 0 && resp.ActualUserToken != "" { + if resp.ActualUserID != 0 && resp.ActualUserToken != "" { /// This means the purchase was made on a different account and we need to switch to that account - slog.Info("Purchase made on a different account, switching accounts", "actualUserId", resp.ActualUserId) + slog.Info("Purchase made on a different account, switching accounts", "actualUserId", resp.ActualUserID) //reset all data - settings.Set(settings.UserIDKey, fmt.Sprintf("%d", resp.ActualUserId)) - settings.Set(settings.TokenKey, resp.ActualUserToken) + ctx := context.Background() + _, err := c.Client().PatchSettings(ctx, settings.Settings{ + settings.UserIDKey: fmt.Sprintf("%d", resp.ActualUserID), + settings.TokenKey: resp.ActualUserToken, + }) + if err != nil { + return nil, fmt.Errorf("error updating settings after account switch: %v", err) + } userData, err := FetchUserData() if err != nil { return nil, err @@ -372,16 +419,22 @@ func AcknowledgeApplePurchase(receipt, planII string) ([]byte, error) { if err != nil { return nil, err } - var resp api.VerifySubscriptionResponse + var resp account.VerifySubscriptionResponse if err := json.Unmarshal([]byte(data), &resp); err != nil { return nil, fmt.Errorf("error unmarshalling acknowledge apple purchase response: %v", err) } - if resp.ActualUserId != 0 && resp.ActualUserToken != "" { + if resp.ActualUserID != 0 && resp.ActualUserToken != "" { /// This means the purchase was made on a different account and we need to switch to that account - slog.Info("Purchase made on a different account, switching accounts", "actualUserId", resp.ActualUserId) + slog.Info("Purchase made on a different account, switching accounts", "actualUserId", resp.ActualUserID) //reset all data - settings.Set(settings.UserIDKey, fmt.Sprintf("%d", resp.ActualUserId)) - settings.Set(settings.TokenKey, resp.ActualUserToken) + ctx := context.Background() + _, err := c.Client().PatchSettings(ctx, settings.Settings{ + settings.UserIDKey: fmt.Sprintf("%d", resp.ActualUserID), + settings.TokenKey: resp.ActualUserToken, + }) + if err != nil { + return nil, fmt.Errorf("error updating settings after account switch: %v", err) + } userData, err := FetchUserData() if err != nil { return nil, err diff --git a/lantern-core/private-server/server.go b/lantern-core/private-server/server.go index adf5775877..6699d2923d 100644 --- a/lantern-core/private-server/server.go +++ b/lantern-core/private-server/server.go @@ -14,8 +14,7 @@ import ( "sync" "time" - "github.com/getlantern/common" - "github.com/getlantern/radiance/servers" + "github.com/getlantern/radiance/ipc" pcommon "github.com/getlantern/lantern-server-provisioner/common" "github.com/getlantern/lantern-server-provisioner/digitalocean" @@ -39,7 +38,7 @@ type provisionSession struct { userProjectString string serverName string serverLocation string - manager *servers.Manager + client *ipc.Client } type provisionerResponse struct { @@ -75,7 +74,7 @@ func getSession() (*provisionSession, error) { // StartDigitalOceanPrivateServerFlow initializes the DigitalOcean provisioner and starts listening for events. // It takes a PrivateServerEventListener to handle events and browser opening. // It returns an error if the provisioner fails to start or if there are issues during the session. -func StartDigitalOceanPrivateServerFlow(events utils.PrivateServerEventListener, vpnClient *servers.Manager) error { +func StartDigitalOceanPrivateServerFlow(events utils.PrivateServerEventListener, vpnClient *ipc.Client) error { ctx := context.Background() provisioner := digitalocean.GetProvisioner(ctx, func(url string) error { return events.OpenBrowser(url) @@ -88,7 +87,7 @@ func StartDigitalOceanPrivateServerFlow(events utils.PrivateServerEventListener, ps := &provisionSession{ provisioner: provisioner, eventSink: events, - manager: vpnClient, + client: vpnClient, } storeSession(ps) go listenToServerEvents(*ps) @@ -96,7 +95,7 @@ func StartDigitalOceanPrivateServerFlow(events utils.PrivateServerEventListener, } // StartGoogleCloudPrivateServerFlow initializes the GCP provisioner and starts listening for events -func StartGoogleCloudPrivateServerFlow(events utils.PrivateServerEventListener, vpnClient *servers.Manager) error { +func StartGoogleCloudPrivateServerFlow(events utils.PrivateServerEventListener, vpnClient *ipc.Client) error { ctx := context.Background() provisioner := gcp.GetProvisioner(ctx, func(url string) error { return events.OpenBrowser(url) @@ -109,7 +108,7 @@ func StartGoogleCloudPrivateServerFlow(events utils.PrivateServerEventListener, ps := &provisionSession{ provisioner: provisioner, eventSink: events, - manager: vpnClient, + client: vpnClient, } storeSession(ps) go listenToServerEvents(*ps) @@ -226,14 +225,20 @@ func listenToServerEvents(ps provisionSession) { // sgp1 - SG [SG] region, city, country := ParseLocation(provisioner.serverLocation) slog.Debug("Provisioner response", slog.Any("response", resp), slog.String("region", region), slog.String("country", country), slog.String("city", city)) - mangerErr := provisioner.manager.AddPrivateServer(resp.Tag, resp.ExternalIP, resp.Port, resp.AccessToken, &common.ServerLocation{CountryCode: country, City: city}, false) + ctx := context.Background() + mangerErr := provisioner.client.AddPrivateServer(ctx, resp.Tag, resp.ExternalIP, resp.Port, resp.AccessToken) if mangerErr != nil { slog.Error("Error adding server manager instance", slog.Any("error", mangerErr)) events.OnError(convertErrorToJSON("EventTypeProvisioningError", mangerErr)) return } slog.Debug("Server manager instance added successfully", slog.String("tag", resp.Tag)) - serverInfo, found := ps.manager.GetServerByTag(resp.Tag) + serverInfo, found, err := ps.client.GetServerByTag(ctx, resp.Tag) + if err != nil { + slog.Error("Error getting server by tag", slog.Any("error", err)) + events.OnError(convertErrorToJSON("EventTypeProvisioningError", err)) + return + } // add protocol info if found if found { resp.Protocol = serverInfo.Type @@ -338,7 +343,7 @@ func CancelDeployment() error { // AddServerManually adds a server manually to the VPN client. // It takes the server's IP, port, access token, and tag, along with the VPN client and event listener. -func AddServerManually(ip, port, accessToken, tag string, vpnClient *servers.Manager, events utils.PrivateServerEventListener) error { +func AddServerManually(ip, port, accessToken, tag string, vpnClient *ipc.Client, events utils.PrivateServerEventListener) error { slog.Debug("Adding server manually", slog.String("ip", ip), slog.String("port", port), slog.String("tag", tag)) portInt, err := strconv.Atoi(port) if err != nil { @@ -351,17 +356,14 @@ func AddServerManually(ip, port, accessToken, tag string, vpnClient *servers.Man Tag: tag, } provisionSession := &provisionSession{ - manager: vpnClient, + client: vpnClient, eventSink: events, } storeSession(provisionSession) location := getGeoInfo(ip) - _, city, country := ParseLocation(location) - err = provisionSession.manager.AddPrivateServer(resp.Tag, resp.ExternalIP, resp.Port, resp.AccessToken, &common.ServerLocation{ - Country: "", - City: city, - CountryCode: country, - }, true) + _, _, _ = ParseLocation(location) + ctx := context.Background() + err = provisionSession.client.AddPrivateServer(ctx, resp.Tag, resp.ExternalIP, resp.Port, resp.AccessToken) if err != nil { return err } diff --git a/lantern-core/utils/common.go b/lantern-core/utils/common.go index 934f374299..8ae91b1218 100644 --- a/lantern-core/utils/common.go +++ b/lantern-core/utils/common.go @@ -1,10 +1,6 @@ package utils import ( - "log/slog" - "os" - - "github.com/getlantern/radiance/issue" "github.com/sagernet/sing-box/experimental/libbox" ) @@ -16,6 +12,7 @@ type Opts struct { Locale string Env string TelemetryConsent bool + Platform PlatformInterface } type PrivateServerEventListener interface { @@ -34,23 +31,6 @@ type FlutterEventEmitter interface { SendEvent(event *FlutterEvent) } -// CreateLogAttachment tries to read the log file at logFilePath and returns -// an []*issue.Attachment with the log (if found) -func CreateLogAttachment(logFilePath string) []*issue.Attachment { - if logFilePath == "" { - return nil - } - data, err := os.ReadFile(logFilePath) - if err != nil { - slog.Debug("could not read log file %q: %v", logFilePath, err) - return nil - } - return []*issue.Attachment{{ - Name: "flutter.log", - Data: data, - }} -} - type PlatformInterface interface { libbox.PlatformInterface RestartService() error diff --git a/lantern-core/vpn_tunnel/vpn_tunnel.go b/lantern-core/vpn_tunnel/vpn_tunnel.go index ed10e090a9..88ea10fe93 100644 --- a/lantern-core/vpn_tunnel/vpn_tunnel.go +++ b/lantern-core/vpn_tunnel/vpn_tunnel.go @@ -1,18 +1,13 @@ package vpn_tunnel import ( + "context" "fmt" - "runtime" - "sync/atomic" - "log/slog" + "github.com/getlantern/radiance/ipc" "github.com/getlantern/radiance/servers" "github.com/getlantern/radiance/vpn" - "github.com/getlantern/radiance/vpn/ipc" - "github.com/getlantern/radiance/vpn/rvpn" - - "github.com/getlantern/lantern/lantern-core/utils" ) type InternalTag string @@ -23,101 +18,51 @@ const ( InternalTagLantern InternalTag = InternalTag(servers.SGLantern) ) -var ipcServer atomic.Pointer[ipc.Server] - -// StartVPN will start the VPN tunnel using the provided platform interface. -// It passes the empty string so it will connect to best server available. -func StartVPN(platform rvpn.PlatformInterface, opts *utils.Opts) error { - // As soon user connects to VPN, we start listening for auto location changes. +func StartVPN(client *ipc.Client) error { slog.Info("StartVPN called") - if err := initIPC(opts, platform); err != nil { - return fmt.Errorf("failed to initialize IPC server: %w", err) - } - // it should use InternalTagLantern so it will connect to best lantern server by default. - // if you want to connect to user server, use ConnectToServer with InternalTagUser - err := vpn.AutoConnect("") - if err != nil { + ctx := context.Background() + if err := client.ConnectVPN(ctx, vpn.AutoSelectTag); err != nil { return fmt.Errorf("failed to start VPN: %w", err) } return nil } -// StopVPN will stop the VPN tunnel. -func StopVPN() error { - return vpn.Disconnect() +func StopVPN(client *ipc.Client) error { + ctx := context.Background() + return client.DisconnectVPN(ctx) } -// ConnectToServer will connect to a specific VPN server identified by the group and tag. If tag is -// empty, it will connect to the best server available in that group. ConnectToServer will start the -// VPN tunnel if it's not already running. -func ConnectToServer(group, tag string, platIfce rvpn.PlatformInterface, opts *utils.Opts) error { - slog.Debug("ConnectToServer called", "group", group, "tag", tag) - if err := initIPC(opts, platIfce); err != nil { - return fmt.Errorf("failed to initialize IPC server: %w", err) - } - switch group { - case string(InternalTagAutoAll), "auto": - group = "all" - case "privateServer": - group = string(InternalTagUser) - case "lanternLocation": - group = string(InternalTagLantern) - } - slog.Debug("Connecting to VPN server", "group", group, "tag", tag) - if tag == "" { - return vpn.QuickConnect(group, platIfce) - } - slog.Debug("Connecting to specific VPN server", "group", group, "tag", tag) - return vpn.ConnectToServer(group, tag, platIfce) +func ConnectToServer(client *ipc.Client, _, tag string) error { + ctx := context.Background() + slog.Debug("Connecting to VPN server", "tag", tag) + return client.SelectServer(ctx, tag) } -func IsVPNRunning() bool { +func IsVPNRunning(client *ipc.Client) bool { slog.Debug("Checking if VPN is running...") - status, err := vpn.GetStatus() + ctx := context.Background() + status, err := client.VPNStatus(ctx) slog.Debug("VPN status:", "status", status, "Error:", err) - return status.TunnelOpen + return status == vpn.Connected } -func GetSelectedServer() string { +func GetSelectedServer(client *ipc.Client) string { slog.Debug("Getting selected VPN server...") - status, err := vpn.GetStatus() - slog.Debug("VPN status:", "status", status, "Error:", err) - return status.SelectedServer -} - -func CloseIPC() error { - if runtime.GOOS == "linux" { - return nil - } - if svr := ipcServer.Swap(nil); svr != nil { - return svr.Close() - } - return nil -} - -func initIPC(opts *utils.Opts, platIfce rvpn.PlatformInterface) error { - if runtime.GOOS == "linux" { - return nil - } - if ipcServer.Load() != nil { - return nil - } - slog.Debug("Initializing IPC", "dataDir", opts.DataDir, "logDir", opts.LogDir, "logLevel", opts.LogLevel) - svr, err := vpn.InitIPC(opts.DataDir, opts.LogDir, opts.LogLevel, platIfce) + ctx := context.Background() + server, _, err := client.SelectedServer(ctx) if err != nil { - return err + slog.Debug("Error getting selected server:", "error", err) + return "" } - ipcServer.Store(svr) - return nil + return server.Tag } -// GetAutoLocation returns the current auto location as a JSON string. -func GetAutoLocation() (*vpn.AutoSelections, error) { +func GetAutoLocation(client *ipc.Client) (*servers.Server, error) { slog.Debug("Getting auto location...") - location, err := vpn.AutoServerSelections() - slog.Debug("Auto location:", "location", location, "Error:", err) + ctx := context.Background() + server, err := client.AutoSelected(ctx) if err != nil { return nil, fmt.Errorf("failed to get auto location: %w", err) } - return &location, nil + return &server, nil } diff --git a/lantern-core/wintunmgr/service_windows.go b/lantern-core/wintunmgr/service_windows.go index cbb62e0b2e..2b4b09a2e4 100644 --- a/lantern-core/wintunmgr/service_windows.go +++ b/lantern-core/wintunmgr/service_windows.go @@ -20,11 +20,13 @@ import ( "time" "github.com/Microsoft/go-winio" + "github.com/getlantern/radiance/backend" + "github.com/getlantern/radiance/events" + "github.com/getlantern/radiance/ipc" + "github.com/getlantern/radiance/vpn" + "github.com/getlantern/lantern/lantern-core/common" - "github.com/getlantern/lantern/lantern-core/utils" "github.com/getlantern/lantern/lantern-core/vpn_tunnel" - "github.com/getlantern/radiance/events" - "github.com/getlantern/radiance/vpn/ipc" ) type ServiceOptions struct { @@ -38,9 +40,11 @@ type ServiceOptions struct { // Service hosts the command server and manages LanternCore // It proxies privileged commands and interacts with Radiance IPC when available type Service struct { - opts ServiceOptions - wtmgr *Manager - cancel context.CancelFunc + opts ServiceOptions + wtmgr *Manager + cancel context.CancelFunc + ipcClient *ipc.Client + ipcServer *ipc.Server } type statusEvent struct { @@ -71,8 +75,9 @@ func (ce *concurrentEncoder) Encode(v any) error { func NewService(opts ServiceOptions, wt *Manager) *Service { return &Service{ - opts: opts, - wtmgr: wt, + opts: opts, + wtmgr: wt, + ipcClient: ipc.NewClient(), } } @@ -114,6 +119,20 @@ func (s *Service) Start(ctx context.Context) error { slog.Info("Starting Windows service", "pipe", s.opts.PipeName, "data_dir", s.opts.DataDir, "log_dir", s.opts.LogDir, "token_path", s.opts.TokenPath) + bopts := backend.Options{ + DataDir: s.opts.DataDir, + LogDir: s.opts.LogDir, + Locale: s.opts.Locale, + LogLevel: "trace", + } + be, err := backend.NewLocalBackend(context.Background(), bopts) + if err != nil { + return fmt.Errorf("initializing ipc server backend: %w", err) + } + s.ipcServer = ipc.NewServer(be, true) + if err := s.ipcServer.Start(); err != nil { + return fmt.Errorf("starting ipc server: %w", err) + } token, err := s.getToken() if err != nil { return fmt.Errorf("token: %w", err) @@ -154,11 +173,11 @@ func (s *Service) Start(ctx context.Context) error { } func (s *Service) handleWatchStatus(ctx context.Context, enc *concurrentEncoder) { - sub := events.Subscribe(func(evt ipc.StatusUpdateEvent) { - slog.Debug("Sending status event", "state", evt.Status.String(), "error", evt.Error) - se := statusEvent{Event: "Status", State: evt.Status.String(), Ts: time.Now().Unix()} - if evt.Error != nil { - se.Error = evt.Error.Error() + sub := events.Subscribe(func(evt vpn.StatusUpdateEvent) { + slog.Debug("Sending status event", "state", string(evt.Status), "error", evt.Error) + se := statusEvent{Event: "Status", State: string(evt.Status), Ts: time.Now().Unix()} + if evt.Error != "" { + se.Error = evt.Error } _ = enc.Encode(se) }) @@ -342,35 +361,35 @@ func (s *Service) dispatch(ctx context.Context, r *Request) *Response { case common.CmdStartTunnel: go func() { - events.Emit(ipc.StatusUpdateEvent{Status: ipc.Connecting}) - if err := vpn_tunnel.StartVPN(nil, &utils.Opts{LogLevel: "trace"}); err != nil { + events.Emit(vpn.StatusUpdateEvent{Status: vpn.Connecting}) + if err := s.ipcClient.ConnectVPN(context.Background(), vpn.AutoSelectTag); err != nil { slog.Error("Error starting service", "error", err) - events.Emit(ipc.StatusUpdateEvent{Status: ipc.ErrorStatus, Error: err}) + events.Emit(vpn.StatusUpdateEvent{Status: vpn.ErrorStatus, Error: err.Error()}) } else { - events.Emit(ipc.StatusUpdateEvent{Status: ipc.Connected}) + events.Emit(vpn.StatusUpdateEvent{Status: vpn.Connected}) } }() return &Response{ID: r.ID, Result: map[string]any{"started": true}} case common.CmdStopTunnel: go func() { - events.Emit(ipc.StatusUpdateEvent{Status: ipc.Disconnecting}) - if err := vpn_tunnel.StopVPN(); err != nil { + events.Emit(vpn.StatusUpdateEvent{Status: vpn.Disconnecting}) + if err := s.ipcClient.DisconnectVPN(context.Background()); err != nil { slog.Error("Error stopping service", "error", err) - events.Emit(ipc.StatusUpdateEvent{Status: ipc.ErrorStatus, Error: err}) + events.Emit(vpn.StatusUpdateEvent{Status: vpn.ErrorStatus, Error: err.Error()}) } else { - events.Emit(ipc.StatusUpdateEvent{Status: ipc.Disconnected}) + events.Emit(vpn.StatusUpdateEvent{Status: vpn.Disconnected}) } }() return &Response{ID: r.ID, Result: map[string]any{"stopped": true}} case common.CmdIsVPNRunning: - running := vpn_tunnel.IsVPNRunning() + running := vpn_tunnel.IsVPNRunning(s.ipcClient) go func() { if running { - events.Emit(ipc.StatusUpdateEvent{Status: ipc.Connected}) + events.Emit(vpn.StatusUpdateEvent{Status: vpn.Connected}) } else { - events.Emit(ipc.StatusUpdateEvent{Status: ipc.Disconnected}) + events.Emit(vpn.StatusUpdateEvent{Status: vpn.Disconnected}) } }() return &Response{ID: r.ID, Result: map[string]any{"running": running}} @@ -385,12 +404,12 @@ func (s *Service) dispatch(ctx context.Context, r *Request) *Response { } group := strings.TrimSpace(p.Location) go func(group, tag string) { - events.Emit(ipc.StatusUpdateEvent{Status: ipc.Connecting}) - if err := vpn_tunnel.ConnectToServer(group, p.Tag, nil, &utils.Opts{LogLevel: "trace"}); err != nil { + events.Emit(vpn.StatusUpdateEvent{Status: vpn.Connecting}) + if err := s.ipcClient.ConnectVPN(context.Background(), tag); err != nil { slog.Error("Error connecting to server", "error", err) - events.Emit(ipc.StatusUpdateEvent{Status: ipc.ErrorStatus, Error: err}) + events.Emit(vpn.StatusUpdateEvent{Status: vpn.ErrorStatus, Error: err.Error()}) } else { - events.Emit(ipc.StatusUpdateEvent{Status: ipc.Connected}) + events.Emit(vpn.StatusUpdateEvent{Status: vpn.Connected}) } }(group, p.Tag) return &Response{ID: r.ID, Result: "ok"} diff --git a/lib/core/extensions/plan.dart b/lib/core/extensions/plan.dart index f23df2f1fe..05da9113fc 100644 --- a/lib/core/extensions/plan.dart +++ b/lib/core/extensions/plan.dart @@ -2,7 +2,7 @@ import 'package:intl/intl.dart'; import 'package:lantern/core/common/common.dart'; import 'package:lantern/core/models/plan_data.dart'; import 'package:lantern/core/utils/currency_utils.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; final _ddmmyyFormatter = DateFormat('dd/MM/yy'); final _mmddyyFormatter = DateFormat('MM/dd/yy'); @@ -31,11 +31,10 @@ extension PlanExtension on Plan { } } -extension IsoDateFormatter on UserResponse_UserData { +extension IsoDateFormatter on UserDataModel { String toDate() { try { if (userLevel == 'expired') { - final lastExpiredOn = this.lastExpiredOn.toInt(); if (lastExpiredOn <= 0) { return "N/A"; } @@ -48,19 +47,18 @@ extension IsoDateFormatter on UserResponse_UserData { } final autoRenew = subscriptionData.autoRenew; - final endAt = subscriptionData.endAt.toInt(); + final endAt = subscriptionData.endAt; // Validate expiration exists if (expiration <= 0) { return "N/A"; } if (autoRenew && endAt != 0) { // Active subscription case - final endAtTimestamp = subscriptionData.endAt.toInt(); - if (endAtTimestamp <= 0) { + if (endAt <= 0) { return "N/A"; } final dateTime = DateTime.fromMillisecondsSinceEpoch( - endAtTimestamp * 1000, + endAt * 1000, isUtc: true, ).toLocal(); @@ -68,7 +66,7 @@ extension IsoDateFormatter on UserResponse_UserData { } // Non-subscription plan case final expirationDate = DateTime.fromMillisecondsSinceEpoch( - expiration.toInt() * 1000, + expiration * 1000, isUtc: true, ).toLocal(); final formattedDate = _formatDate(expirationDate); diff --git a/lib/core/extensions/user_data.dart b/lib/core/extensions/user_data.dart index 2e76fca634..b5b92fea44 100644 --- a/lib/core/extensions/user_data.dart +++ b/lib/core/extensions/user_data.dart @@ -1,5 +1,5 @@ -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; -extension UserDataProX on UserResponse_UserData { +extension UserDataProX on UserDataModel { bool get isPro => userLevel == 'pro'; } diff --git a/lib/core/models/user.dart b/lib/core/models/user.dart index 768f3d01e0..78eebbca30 100644 --- a/lib/core/models/user.dart +++ b/lib/core/models/user.dart @@ -1,22 +1,25 @@ class UserResponseModel { + final String id; final int legacyID; final String legacyToken; final bool emailConfirmed; final bool success; - final UserDataModel? legacyUserData; + final UserDataModel legacyUserData; final List devices; const UserResponseModel({ + this.id = '', required this.legacyID, required this.legacyToken, required this.emailConfirmed, required this.success, - this.legacyUserData, + this.legacyUserData = const UserDataModel(), this.devices = const [], }); factory UserResponseModel.fromJson(Map json) => UserResponseModel( + id: (json['id'] ?? '').toString(), legacyID: (json['legacyID'] as num?)?.toInt() ?? 0, legacyToken: (json['legacyToken'] ?? '').toString(), emailConfirmed: json['emailConfirmed'] == true, @@ -25,7 +28,7 @@ class UserResponseModel { ? UserDataModel.fromJson( Map.from(json['legacyUserData'] as Map), ) - : null, + : const UserDataModel(), devices: ((json['devices'] as List?) ?? const []) .whereType() .map((m) => DeviceModel.fromJson(Map.from(m))) @@ -33,11 +36,12 @@ class UserResponseModel { ); Map toJson() => { + 'id': id, 'legacyID': legacyID, 'legacyToken': legacyToken, 'emailConfirmed': emailConfirmed, 'success': success, - 'legacyUserData': legacyUserData?.toJson(), + 'legacyUserData': legacyUserData.toJson(), 'devices': devices.map((d) => d.toJson()).toList(), }; } @@ -85,36 +89,36 @@ class UserDataModel { final String inviters; final String invitees; final List devices; - final String purchases; // consider List instead - final SubscriptionDataModel? subscriptionData; + final String purchases; + final SubscriptionDataModel subscriptionData; final String deviceID; final bool unpassRegistered; final int lastExpiredOn; const UserDataModel({ - required this.userId, - required this.code, - required this.token, - required this.referral, - required this.phone, - required this.email, - required this.userStatus, - required this.userLevel, - required this.locale, - required this.expiration, - required this.subscription, - required this.bonusDays, - required this.bonusMonths, - required this.yinbiEnabled, - required this.servers, - required this.inviters, - required this.invitees, - required this.purchases, - required this.deviceID, - required this.unpassRegistered, - required this.lastExpiredOn, + this.userId = 0, + this.code = '', + this.token = '', + this.referral = '', + this.phone = '', + this.email = '', + this.userStatus = '', + this.userLevel = '', + this.locale = '', + this.expiration = 0, + this.subscription = '', + this.bonusDays = '', + this.bonusMonths = '', + this.yinbiEnabled = false, + this.servers = '', + this.inviters = '', + this.invitees = '', + this.purchases = '', + this.deviceID = '', + this.unpassRegistered = false, + this.lastExpiredOn = 0, this.devices = const [], - this.subscriptionData, + this.subscriptionData = const SubscriptionDataModel(), }); factory UserDataModel.fromJson(Map json) => UserDataModel( @@ -147,7 +151,7 @@ class UserDataModel { ? SubscriptionDataModel.fromJson( Map.from(json['subscriptionData'] as Map), ) - : null, + : const SubscriptionDataModel(), ); Map toJson() => { @@ -170,7 +174,7 @@ class UserDataModel { 'invitees': invitees, 'devices': devices.map((d) => d.toJson()).toList(), 'purchases': purchases, - 'subscriptionData': subscriptionData?.toJson(), + 'subscriptionData': subscriptionData.toJson(), 'deviceID': deviceID, 'unpassRegistered': unpassRegistered, 'lastExpiredOn': lastExpiredOn, @@ -180,40 +184,40 @@ class UserDataModel { class SubscriptionDataModel { final String planID; final String stripeCustomerID; - final String startAt; - final String cancelledAt; + final int startAt; + final int cancelledAt; final bool autoRenew; final String subscriptionID; final String status; final String provider; - final String createdAt; - final String endAt; + final int createdAt; + final int endAt; const SubscriptionDataModel({ - required this.planID, - required this.stripeCustomerID, - required this.startAt, - required this.cancelledAt, - required this.autoRenew, - required this.subscriptionID, - required this.status, - required this.provider, - required this.createdAt, - required this.endAt, + this.planID = '', + this.stripeCustomerID = '', + this.startAt = 0, + this.cancelledAt = 0, + this.autoRenew = false, + this.subscriptionID = '', + this.status = '', + this.provider = '', + this.createdAt = 0, + this.endAt = 0, }); factory SubscriptionDataModel.fromJson(Map json) => SubscriptionDataModel( planID: (json['planID'] ?? '').toString(), stripeCustomerID: (json['stripeCustomerID'] ?? '').toString(), - startAt: (json['startAt'] ?? '').toString(), - cancelledAt: (json['cancelledAt'] ?? '').toString(), + startAt: (json['startAt'] as num?)?.toInt() ?? 0, + cancelledAt: (json['cancelledAt'] as num?)?.toInt() ?? 0, autoRenew: json['autoRenew'] == true, subscriptionID: (json['subscriptionID'] ?? '').toString(), status: (json['status'] ?? '').toString(), provider: (json['provider'] ?? '').toString(), - createdAt: (json['createdAt'] ?? '').toString(), - endAt: (json['endAt'] ?? '').toString(), + createdAt: (json['createdAt'] as num?)?.toInt() ?? 0, + endAt: (json['endAt'] as num?)?.toInt() ?? 0, ); Map toJson() => { diff --git a/lib/core/router/router.gr.dart b/lib/core/router/router.gr.dart index bd0ac5e42f..9437aabab5 100644 --- a/lib/core/router/router.gr.dart +++ b/lib/core/router/router.gr.dart @@ -13,6 +13,7 @@ import 'package:auto_route/auto_route.dart' as _i44; import 'package:collection/collection.dart' as _i48; import 'package:flutter/material.dart' as _i45; import 'package:lantern/core/common/common.dart' as _i46; +import 'package:lantern/core/models/user.dart' as _i47; import 'package:lantern/core/widgets/app_webview.dart' as _i3; import 'package:lantern/features/account/account.dart' as _i1; import 'package:lantern/features/account/delete_account.dart' as _i9; @@ -68,7 +69,6 @@ import 'package:lantern/features/split_tunneling/website_split_tunneling.dart' as _i43; import 'package:lantern/features/support/support.dart' as _i41; import 'package:lantern/features/vpn/server_selection.dart' as _i34; -import 'package:lantern/lantern/protos/protos/auth.pb.dart' as _i47; /// generated route for /// [_i1.Account] @@ -483,7 +483,7 @@ class DeveloperMode extends _i44.PageRouteInfo { class DeviceLimitReached extends _i44.PageRouteInfo { DeviceLimitReached({ _i45.Key? key, - required List<_i47.UserResponse_Device> devices, + required List<_i47.DeviceModel> devices, List<_i44.PageRouteInfo>? children, }) : super( DeviceLimitReached.name, @@ -507,7 +507,7 @@ class DeviceLimitReachedArgs { final _i45.Key? key; - final List<_i47.UserResponse_Device> devices; + final List<_i47.DeviceModel> devices; @override String toString() { @@ -519,7 +519,7 @@ class DeviceLimitReachedArgs { if (identical(this, other)) return true; if (other is! DeviceLimitReachedArgs) return false; return key == other.key && - const _i48.ListEquality<_i47.UserResponse_Device>().equals( + const _i48.ListEquality<_i47.DeviceModel>().equals( devices, other.devices, ); @@ -527,8 +527,7 @@ class DeviceLimitReachedArgs { @override int get hashCode => - key.hashCode ^ - const _i48.ListEquality<_i47.UserResponse_Device>().hash(devices); + key.hashCode ^ const _i48.ListEquality<_i47.DeviceModel>().hash(devices); } /// generated route for diff --git a/lib/core/services/app_purchase.dart b/lib/core/services/app_purchase.dart index b3bfc1b5e8..0f01fd3727 100644 --- a/lib/core/services/app_purchase.dart +++ b/lib/core/services/app_purchase.dart @@ -327,9 +327,8 @@ class AppPurchase { } final userLevel = user.legacyUserData.userLevel.toLowerCase(); - final subscriptionStatus = user.legacyUserData.hasSubscriptionData() - ? user.legacyUserData.subscriptionData.status.toLowerCase() - : ''; + final subscriptionStatus = + user.legacyUserData.subscriptionData.status.toLowerCase(); return userLevel == 'pro' || subscriptionStatus == 'active'; } diff --git a/lib/core/widgets/user_devices.dart b/lib/core/widgets/user_devices.dart index b66bba203a..db66667414 100644 --- a/lib/core/widgets/user_devices.dart +++ b/lib/core/widgets/user_devices.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lantern/features/auth/provider/auth_notifier.dart'; import 'package:lantern/features/home/provider/home_notifier.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; import '../common/common.dart'; class UserDevices extends HookConsumerWidget { - // final List userDevices; + // final List userDevices; // final String myDeviceId; const UserDevices({ @@ -21,7 +21,7 @@ class UserDevices extends HookConsumerWidget { return const SizedBox(); } final userDevices = user.legacyUserData.devices.toList(); - final myDeviceId = user.legacyUserData.deviceID ?? ''; + final myDeviceId = user.legacyUserData.deviceID; return AppCard( padding: EdgeInsets.zero, @@ -39,7 +39,7 @@ class UserDevices extends HookConsumerWidget { ); } - Widget _buildRow(UserResponse_Device e, WidgetRef ref, BuildContext context, + Widget _buildRow(DeviceModel e, WidgetRef ref, BuildContext context, bool isMyDevice) { return AppTile( label: e.name, @@ -54,10 +54,10 @@ class UserDevices extends HookConsumerWidget { } Future _removeDevice( - UserResponse_Device device, WidgetRef ref, BuildContext context) async { + DeviceModel device, WidgetRef ref, BuildContext context) async { context.showLoadingDialog(); final result = - await ref.read(authProvider.notifier).deviceRemove(device.id); + await ref.read(authProvider.notifier).deviceRemove(device.deviceId); result.fold((failure) { context.showSnackBar(failure.localizedErrorMessage); diff --git a/lib/features/account/account.dart b/lib/features/account/account.dart index aa019e7b45..c823a20d5f 100644 --- a/lib/features/account/account.dart +++ b/lib/features/account/account.dart @@ -11,7 +11,7 @@ import 'package:lantern/features/home/provider/app_setting_notifier.dart'; import 'package:lantern/features/home/provider/home_notifier.dart'; import 'package:lantern/lantern/lantern_service.dart'; import 'package:lantern/lantern/lantern_service_notifier.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; @RoutePage(name: 'Account') class Account extends HookConsumerWidget { @@ -182,7 +182,7 @@ class Account extends HookConsumerWidget { } Widget? planTrailingWidget( - UserResponse user, BuildContext buildContext, WidgetRef ref) { + UserResponseModel user, BuildContext buildContext, WidgetRef ref) { final autoRenew = user.legacyUserData.subscriptionData.autoRenew; final isUserExpired = user.legacyUserData.userLevel == 'expired'; @@ -201,7 +201,7 @@ class Account extends HookConsumerWidget { } Future onManageSubscriptionTap( - WidgetRef ref, BuildContext buildContext, UserResponse user) async { + WidgetRef ref, BuildContext buildContext, UserResponseModel user) async { final provider = user.legacyUserData.subscriptionData.provider; switch (provider) { case 'apple': @@ -290,7 +290,7 @@ class Account extends HookConsumerWidget { } Future _handleSubscriptionChange( - {required UserResponse oldUser, + {required UserResponseModel oldUser, required LanternService lanternService, required HomeNotifier notifier, required BuildContext context}) async { diff --git a/lib/features/auth/device_limit_reached.dart b/lib/features/auth/device_limit_reached.dart index 0772287b76..f4bd7caca9 100644 --- a/lib/features/auth/device_limit_reached.dart +++ b/lib/features/auth/device_limit_reached.dart @@ -5,11 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lantern/core/common/common.dart'; import 'package:lantern/features/auth/provider/auth_notifier.dart'; -import '../../lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; @RoutePage(name: 'DeviceLimitReached') class DeviceLimitReached extends HookConsumerWidget { - final List devices; + final List devices; const DeviceLimitReached({ super.key, @@ -19,7 +19,7 @@ class DeviceLimitReached extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final textTheme = Theme.of(context).textTheme; - final selectedDevice = useState(null); + final selectedDevice = useState(null); return BaseScreen( title: 'device_limit_reached'.i18n, body: Column( @@ -52,7 +52,7 @@ class DeviceLimitReached extends HookConsumerWidget { AppTile( contentPadding: EdgeInsets.zero, label: device.name, - trailing: AppRadioButton( + trailing: AppRadioButton( value: device, groupValue: selectedDevice.value, onChanged: (value) { @@ -73,7 +73,7 @@ class DeviceLimitReached extends HookConsumerWidget { isTaller: true, enabled: selectedDevice.value != null, onPressed: () => - removeDeviceAndLogin(ref, selectedDevice.value!.id, context), + removeDeviceAndLogin(ref, selectedDevice.value!.deviceId, context), ), SizedBox(height: 30.0), Center( diff --git a/lib/features/auth/provider/auth_notifier.dart b/lib/features/auth/provider/auth_notifier.dart index 0ed24c4008..9698b72e53 100644 --- a/lib/features/auth/provider/auth_notifier.dart +++ b/lib/features/auth/provider/auth_notifier.dart @@ -1,7 +1,7 @@ import 'package:fpdart/fpdart.dart'; import 'package:lantern/core/utils/failure.dart'; import 'package:lantern/lantern/lantern_service_notifier.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'auth_notifier.g.dart'; @@ -17,12 +17,12 @@ class AuthNotifier extends _$AuthNotifier { return ref.read(lanternServiceProvider).getOAuthLoginUrl(provider); } - Future> oAuthLoginCallback( + Future> oAuthLoginCallback( String authToken) async { return ref.read(lanternServiceProvider).oAuthLoginCallback(authToken); } - Future> signInWithEmail( + Future> signInWithEmail( String email, String password) async { return ref.read(lanternServiceProvider).login( email: email, @@ -73,7 +73,7 @@ class AuthNotifier extends _$AuthNotifier { newEmail: newEmail, password: password, code: code); } - Future> deleteAccount( + Future> deleteAccount( String email, String password, bool isSSO) async { return ref .read(lanternServiceProvider) diff --git a/lib/features/auth/provider/auth_notifier.g.dart b/lib/features/auth/provider/auth_notifier.g.dart index 2d8426f23d..bc5e52f9d3 100644 --- a/lib/features/auth/provider/auth_notifier.g.dart +++ b/lib/features/auth/provider/auth_notifier.g.dart @@ -33,7 +33,7 @@ final class AuthNotifierProvider AuthNotifier create() => AuthNotifier(); } -String _$authNotifierHash() => r'99d7ee9c20a5e4fecf7748c135b3d20e274244ec'; +String _$authNotifierHash() => r'66b44d2581632b51f156ceb934b5674bbb7d66d9'; abstract class _$AuthNotifier extends $AsyncNotifier { FutureOr build(); diff --git a/lib/features/auth/sign_in_password.dart b/lib/features/auth/sign_in_password.dart index 87e90d17f8..87a9bac859 100644 --- a/lib/features/auth/sign_in_password.dart +++ b/lib/features/auth/sign_in_password.dart @@ -7,7 +7,7 @@ import 'package:lantern/core/widgets/email_tag.dart'; import 'package:lantern/features/auth/provider/auth_notifier.dart'; import 'package:lantern/features/home/provider/app_setting_notifier.dart'; import 'package:lantern/features/home/provider/home_notifier.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; @RoutePage(name: 'SignInPassword') class SignInPassword extends StatefulHookConsumerWidget { @@ -159,7 +159,7 @@ class _SignInPasswordState extends ConsumerState { ); } - void startDeviceFlow(List devices, String password, + void startDeviceFlow(List devices, String password, BuildContext context) { appRouter.push(DeviceLimitReached(devices: devices)).then( (value) { diff --git a/lib/features/home/provider/home_notifier.dart b/lib/features/home/provider/home_notifier.dart index 0e121719aa..5af47324c7 100644 --- a/lib/features/home/provider/home_notifier.dart +++ b/lib/features/home/provider/home_notifier.dart @@ -4,7 +4,7 @@ import 'package:lantern/core/extensions/user_data.dart'; import 'package:lantern/features/home/provider/app_setting_notifier.dart'; import 'package:lantern/features/plans/provider/referral_notifier.dart'; import 'package:lantern/features/vpn/provider/server_location_notifier.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; import 'package:lantern/lantern/lantern_service_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -13,7 +13,7 @@ part 'home_notifier.g.dart'; @Riverpod(keepAlive: true) class HomeNotifier extends _$HomeNotifier { @override - Future build() async { + Future build() async { /// Check if user data is stored locally /// If yes, load it first to avoid delay in UI final result = await ref.read(lanternServiceProvider).getUserData(); @@ -68,22 +68,17 @@ class HomeNotifier extends _$HomeNotifier { /// Updates the user data in state and local storage. /// notifies UI about changes. - void updateUserData(UserResponse userData) { + void updateUserData(UserResponseModel userData) { _applyUserData(userData); } - void _applyUserData(UserResponse userData) { + void _applyUserData(UserResponseModel userData) { state = AsyncValue.data(userData); if (!userData.legacyUserData.isPro) { resetServerLocation(); } - String email; - if (userData.legacyUserData.email.isEmpty) { - email = userData.id; - } else { - email = userData.legacyUserData.email; - } + final email = userData.legacyUserData.email; ref.read(appSettingProvider.notifier).setEmail(email); checkIfUserProAndDeviceIsAdded(); } @@ -126,7 +121,7 @@ class HomeNotifier extends _$HomeNotifier { _checkIfUserProAndDeviceIsAdded(user); } - void _checkIfUserProAndDeviceIsAdded(UserResponse user) { + void _checkIfUserProAndDeviceIsAdded(UserResponseModel user) { if (!user.legacyUserData.isPro) { appLogger.info("User is not Pro. Skipping device check."); return; @@ -138,7 +133,7 @@ class HomeNotifier extends _$HomeNotifier { } final userDeviceId = user.legacyUserData.deviceID; final isDeviceAdded = - user.legacyUserData.devices.any((device) => device.id == userDeviceId); + user.legacyUserData.devices.any((device) => device.deviceId == userDeviceId); appLogger .info("current device added for user ${user.legacyUserData.email}: " "$isDeviceAdded"); diff --git a/lib/features/home/provider/home_notifier.g.dart b/lib/features/home/provider/home_notifier.g.dart index 1bc1091975..d4501a026e 100644 --- a/lib/features/home/provider/home_notifier.g.dart +++ b/lib/features/home/provider/home_notifier.g.dart @@ -13,7 +13,7 @@ part of 'home_notifier.dart'; final homeProvider = HomeNotifierProvider._(); final class HomeNotifierProvider - extends $AsyncNotifierProvider { + extends $AsyncNotifierProvider { HomeNotifierProvider._() : super( from: null, @@ -33,19 +33,20 @@ final class HomeNotifierProvider HomeNotifier create() => HomeNotifier(); } -String _$homeNotifierHash() => r'008a819ad8ed52ef8483e4f8547f347620470f25'; +String _$homeNotifierHash() => r'dda3b69076c84102cc584a85fa68f0b174851fdc'; -abstract class _$HomeNotifier extends $AsyncNotifier { - FutureOr build(); +abstract class _$HomeNotifier extends $AsyncNotifier { + FutureOr build(); @$mustCallSuper @override void runBuild() { - final ref = this.ref as $Ref, UserResponse>; + final ref = + this.ref as $Ref, UserResponseModel>; final element = ref.element as $ClassProviderElement< - AnyNotifier, UserResponse>, - AsyncValue, + AnyNotifier, UserResponseModel>, + AsyncValue, Object?, Object? >; diff --git a/lib/features/split_tunneling/provider/apps_notifier.g.dart b/lib/features/split_tunneling/provider/apps_notifier.g.dart index 6379dfbac3..85dd630ad2 100644 --- a/lib/features/split_tunneling/provider/apps_notifier.g.dart +++ b/lib/features/split_tunneling/provider/apps_notifier.g.dart @@ -34,7 +34,7 @@ final class SplitTunnelingAppsProvider } String _$splitTunnelingAppsHash() => - r'1ac6edd52bfdfd089e6c5e557699f8170e3dc00a'; + r'20d67a407455e04072d6b8636e1d951dad6ebac4'; abstract class _$SplitTunnelingApps extends $AsyncNotifier> { FutureOr> build(); diff --git a/lib/features/system_tray/provider/system_tray_notifier.g.dart b/lib/features/system_tray/provider/system_tray_notifier.g.dart index 26939f7b7d..0386d2d20c 100644 --- a/lib/features/system_tray/provider/system_tray_notifier.g.dart +++ b/lib/features/system_tray/provider/system_tray_notifier.g.dart @@ -34,7 +34,7 @@ final class SystemTrayNotifierProvider } String _$systemTrayNotifierHash() => - r'63fb171a34e7ef783d7bb6675e511dec7638f041'; + r'1e7c38d1b803b39b7e7f44a68f981b274fa8daee'; abstract class _$SystemTrayNotifier extends $AsyncNotifier { FutureOr build(); diff --git a/lib/features/vpn/provider/server_location_notifier.g.dart b/lib/features/vpn/provider/server_location_notifier.g.dart index 9c8d5922bf..9ec8570ea4 100644 --- a/lib/features/vpn/provider/server_location_notifier.g.dart +++ b/lib/features/vpn/provider/server_location_notifier.g.dart @@ -42,7 +42,7 @@ final class ServerLocationNotifierProvider } String _$serverLocationNotifierHash() => - r'cf58012d44d48e3d21c9a56c90c4ae80d724aec6'; + r'22f037c8f1b02d2439bec7f8f2ff7dc188c649eb'; abstract class _$ServerLocationNotifier extends $Notifier { ServerLocation build(); diff --git a/lib/features/vpn/provider/vpn_notifier.g.dart b/lib/features/vpn/provider/vpn_notifier.g.dart index bd3f283e98..7cf82baf24 100644 --- a/lib/features/vpn/provider/vpn_notifier.g.dart +++ b/lib/features/vpn/provider/vpn_notifier.g.dart @@ -41,7 +41,7 @@ final class VpnNotifierProvider } } -String _$vpnNotifierHash() => r'9d5685dcb24bd12386049fdbc1f7aea0db2ebe46'; +String _$vpnNotifierHash() => r'26dafc14cc29a89664e1f411b344da9a008e6cbb'; abstract class _$VpnNotifier extends $Notifier { VPNStatus build(); diff --git a/lib/lantern/lantern_core_service.dart b/lib/lantern/lantern_core_service.dart index a4d2b43e8a..ccdd69955f 100644 --- a/lib/lantern/lantern_core_service.dart +++ b/lib/lantern/lantern_core_service.dart @@ -8,7 +8,7 @@ import 'package:lantern/core/models/lantern_status.dart'; import 'package:lantern/core/models/macos_extension_state.dart'; import 'package:lantern/core/models/plan_data.dart'; import 'package:lantern/core/models/private_server_status.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; import '../core/services/app_purchase.dart'; @@ -116,25 +116,25 @@ abstract class LanternCoreService { ///OAuth methods Future> getOAuthLoginUrl(String provider); - Future> oAuthLoginCallback(String token); + Future> oAuthLoginCallback(String token); Future> activationCode( {required String email, required String resellerCode}); ///User management methods - Future> login( + Future> login( {required String email, required String password}); Future> signUp( {required String email, required String password}); - Future> getUserData(); + Future> getUserData(); - Future> fetchUserData(); + Future> fetchUserData(); Future> getDataCapInfo(); - Future> logout(String email); + Future> logout(String email); //Change email Future> startChangeEmail( @@ -159,7 +159,7 @@ abstract class LanternCoreService { }); //Delete account - Future> deleteAccount( + Future> deleteAccount( {required String email, required String password, bool isSSO = false}); //Device Remove diff --git a/lib/lantern/lantern_ffi_service.dart b/lib/lantern/lantern_ffi_service.dart index a75c46ce7d..7da3bd96a4 100644 --- a/lib/lantern/lantern_ffi_service.dart +++ b/lib/lantern/lantern_ffi_service.dart @@ -22,7 +22,7 @@ import 'package:lantern/lantern/lantern_core_service.dart'; import 'package:lantern/lantern/lantern_generated_bindings.dart'; import 'package:lantern/lantern/lantern_service.dart'; import 'package:lantern/lantern/lantern_windows_service.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; import 'package:path/path.dart' as p; import '../core/models/available_servers.dart'; @@ -891,13 +891,14 @@ class LanternFFIService implements LanternCoreService { } @override - Future> oAuthLoginCallback(String token) async { + Future> oAuthLoginCallback(String token) async { try { final result = await runInBackground(() async { return _ffiService.oAuthLoginCallback(token.toCharPtr).toDartString(); }); - final decodedResult = base64Decode(result); - final user = UserResponse.fromBuffer(decodedResult); + checkAPIError(result); + final map = jsonDecode(result) as Map; + final user = UserResponseModel.fromJson(map); return Right(user); } catch (e, stackTrace) { appLogger.error('error oauth callback', e, stackTrace); @@ -906,16 +907,14 @@ class LanternFFIService implements LanternCoreService { } @override - Future> getUserData() async { - // if (Platform.isWindows) { - // return _windowsService.getUserData(); - // } + Future> getUserData() async { try { final result = await runInBackground(() async { return _ffiService.getUserData().toDartString(); }); - final decodedResult = base64Decode(result); - final user = UserResponse.fromBuffer(decodedResult); + checkAPIError(result); + final map = jsonDecode(result) as Map; + final user = UserResponseModel.fromJson(map); return Right(user); } catch (e, stackTrace) { appLogger.error('Error getting user data', e, stackTrace); @@ -929,13 +928,14 @@ class LanternFFIService implements LanternCoreService { } @override - Future> fetchUserData() async { + Future> fetchUserData() async { try { final result = await runInBackground(() async { return _ffiService.fetchUserData().toDartString(); }); - final decodedResult = base64Decode(result); - final user = UserResponse.fromBuffer(decodedResult); + checkAPIError(result); + final map = jsonDecode(result) as Map; + final user = UserResponseModel.fromJson(map); return Right(user); } catch (e, stackTrace) { appLogger.error('error fetchUser data', e, stackTrace); @@ -975,14 +975,14 @@ class LanternFFIService implements LanternCoreService { } @override - Future> logout(String email) async { + Future> logout(String email) async { try { final result = await runInBackground(() async { return _ffiService.logout(email.toCharPtr).toDartString(); }); checkAPIError(result); - final decodedResult = base64Decode(result); - final user = UserResponse.fromBuffer(decodedResult); + final map = jsonDecode(result) as Map; + final user = UserResponseModel.fromJson(map); return Right(user); } catch (e, stackTrace) { appLogger.error('error while logout', e, stackTrace); @@ -991,7 +991,7 @@ class LanternFFIService implements LanternCoreService { } @override - Future> login({ + Future> login({ required String email, required String password, }) async { @@ -1002,8 +1002,8 @@ class LanternFFIService implements LanternCoreService { .toDartString(); }); checkAPIError(result); - final decodedResult = base64Decode(result); - final user = UserResponse.fromBuffer(decodedResult); + final map = jsonDecode(result) as Map; + final user = UserResponseModel.fromJson(map); return Right(user); } catch (e, stackTrace) { appLogger.error('error while login', e, stackTrace); @@ -1088,7 +1088,7 @@ class LanternFFIService implements LanternCoreService { } @override - Future> deleteAccount({ + Future> deleteAccount({ required String email, required String password, bool isSSO = false, @@ -1100,8 +1100,8 @@ class LanternFFIService implements LanternCoreService { .toDartString(); }); checkAPIError(result); - final decodedResult = base64Decode(result); - final user = UserResponse.fromBuffer(decodedResult); + final map = jsonDecode(result) as Map; + final user = UserResponseModel.fromJson(map); return Right(user); } catch (e, stackTrace) { appLogger.error('Error deleting account', e, stackTrace); diff --git a/lib/lantern/lantern_platform_service.dart b/lib/lantern/lantern_platform_service.dart index be6e3c34d0..70108f2724 100644 --- a/lib/lantern/lantern_platform_service.dart +++ b/lib/lantern/lantern_platform_service.dart @@ -19,7 +19,7 @@ import 'package:lantern/core/utils/app_data_utils.dart'; import 'package:lantern/core/utils/enabled_apps.dart'; import 'package:lantern/lantern/lantern_core_service.dart'; import 'package:lantern/lantern/lantern_ffi_service.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; import '../core/models/lantern_status.dart'; import '../core/services/injection_container.dart' show sl; @@ -727,11 +727,12 @@ class LanternPlatformService implements LanternCoreService { } @override - Future> oAuthLoginCallback(String token) async { + Future> oAuthLoginCallback(String token) async { try { - final bytes = - await _methodChannel.invokeMethod('oauthLoginCallback', token); - return Right(UserResponse.fromBuffer(bytes)); + final result = + await _methodChannel.invokeMethod('oauthLoginCallback', token); + final map = jsonDecode(result!) as Map; + return Right(UserResponseModel.fromJson(map)); } catch (e, stackTrace) { appLogger.error('Error handling OAuth login callback', e, stackTrace); return Left(Failure( @@ -744,10 +745,11 @@ class LanternPlatformService implements LanternCoreService { /// /// Get user data from local storage @override - Future> getUserData() async { + Future> getUserData() async { try { - final bytes = await _methodChannel.invokeMethod('getUserData'); - return Right(UserResponse.fromBuffer(bytes)); + final result = await _methodChannel.invokeMethod('getUserData'); + final map = jsonDecode(result!) as Map; + return Right(UserResponseModel.fromJson(map)); } catch (e, stackTrace) { appLogger.error('Error while getUserData user data', e, stackTrace); return Left(Failure( @@ -758,10 +760,11 @@ class LanternPlatformService implements LanternCoreService { /// Fetch user data from server @override - Future> fetchUserData() async { + Future> fetchUserData() async { try { - final userBytes = await _methodChannel.invokeMethod('fetchUserData'); - return Right(UserResponse.fromBuffer(userBytes)); + final result = await _methodChannel.invokeMethod('fetchUserData'); + final map = jsonDecode(result!) as Map; + return Right(UserResponseModel.fromJson(map)); } catch (e, stackTrace) { appLogger.error("error fetching user data", e, stackTrace); return Left(Failure( @@ -851,14 +854,15 @@ class LanternPlatformService implements LanternCoreService { /// Authentication methods @override - Future> login( + Future> login( {required String email, required String password}) async { try { - final bytes = await _methodChannel.invokeMethod('login', { + final result = await _methodChannel.invokeMethod('login', { 'email': email, 'password': password, }); - return Right(UserResponse.fromBuffer(bytes)); + final map = jsonDecode(result!) as Map; + return Right(UserResponseModel.fromJson(map)); } catch (e) { appLogger.error('Error logging', e); return Left(e.toFailure()); @@ -866,10 +870,11 @@ class LanternPlatformService implements LanternCoreService { } @override - Future> logout(String email) async { + Future> logout(String email) async { try { - final bytes = await _methodChannel.invokeMethod('logout', email); - return Right(UserResponse.fromBuffer(bytes)); + final result = await _methodChannel.invokeMethod('logout', email); + final map = jsonDecode(result!) as Map; + return Right(UserResponseModel.fromJson(map)); } catch (e, stackTrace) { appLogger.error('Error logging out', e, stackTrace); return Left(e.toFailure()); @@ -940,17 +945,18 @@ class LanternPlatformService implements LanternCoreService { } @override - Future> deleteAccount( + Future> deleteAccount( {required String email, required String password, bool isSSO = false}) async { try { - final bytes = await _methodChannel.invokeMethod('deleteAccount', { + final result = await _methodChannel.invokeMethod('deleteAccount', { 'email': email, 'password': password, 'isSSO': isSSO, }); - return Right(UserResponse.fromBuffer(bytes)); + final map = jsonDecode(result!) as Map; + return Right(UserResponseModel.fromJson(map)); } catch (e, stackTrace) { appLogger.error('Error deleting account', e, stackTrace); return Left(e.toFailure()); diff --git a/lib/lantern/lantern_service.dart b/lib/lantern/lantern_service.dart index b002a1e1a2..0bd07c5306 100644 --- a/lib/lantern/lantern_service.dart +++ b/lib/lantern/lantern_service.dart @@ -11,7 +11,7 @@ import 'package:lantern/core/services/app_purchase.dart'; import 'package:lantern/lantern/lantern_core_service.dart'; import 'package:lantern/lantern/lantern_ffi_service.dart'; import 'package:lantern/lantern/lantern_platform_service.dart'; -import 'package:lantern/lantern/protos/protos/auth.pb.dart'; +import 'package:lantern/core/models/user.dart'; import '../core/common/common.dart' hide DeveloperMode; import '../core/models/available_servers.dart'; @@ -266,7 +266,7 @@ class LanternService implements LanternCoreService { } @override - Future> oAuthLoginCallback(String token) { + Future> oAuthLoginCallback(String token) { if (PlatformUtils.isFFISupported) { return _ffiService.oAuthLoginCallback(token); } @@ -274,7 +274,7 @@ class LanternService implements LanternCoreService { } @override - Future> getUserData() { + Future> getUserData() { if (PlatformUtils.isFFISupported) { return _ffiService.getUserData(); } @@ -298,7 +298,7 @@ class LanternService implements LanternCoreService { } @override - Future> fetchUserData() { + Future> fetchUserData() { if (PlatformUtils.isFFISupported) { return _ffiService.fetchUserData(); } @@ -323,7 +323,7 @@ class LanternService implements LanternCoreService { } @override - Future> logout(String email) { + Future> logout(String email) { if (PlatformUtils.isFFISupported) { return _ffiService.logout(email); } @@ -351,7 +351,7 @@ class LanternService implements LanternCoreService { } @override - Future> login({ + Future> login({ required String email, required String password, }) { @@ -412,7 +412,7 @@ class LanternService implements LanternCoreService { } @override - Future> deleteAccount({ + Future> deleteAccount({ required String email, required String password, bool isSSO = false, diff --git a/macos/PacketTunnel/SingBox/ExtensionProvider.swift b/macos/PacketTunnel/SingBox/ExtensionProvider.swift index 977071e819..371fd3c20e 100644 --- a/macos/PacketTunnel/SingBox/ExtensionProvider.swift +++ b/macos/PacketTunnel/SingBox/ExtensionProvider.swift @@ -29,6 +29,15 @@ public class ExtensionProvider: NEPacketTunnelProvider { if platformInterface == nil { platformInterface = ExtensionPlatformInterface(self) } + + // Start the IPC server before any VPN operations + var ipcError: NSError? + MobileStartIPCServer(platformInterface, opts(), &ipcError) + if let ipcError { + appLogger.error("error starting IPC server: \(ipcError.localizedDescription)") + throw ipcError + } + let tunnelType = options?["netEx.Type"] as? String switch tunnelType { case "Lantern": @@ -100,9 +109,9 @@ public class ExtensionProvider: NEPacketTunnelProvider { if error != nil { appLogger.log("error while stopping tunnel \(error?.localizedDescription ?? "")") } - MobileCloseIPC(&error) + MobileCloseIPCServer(&error) if error != nil { - appLogger.log("error closing IPC \(error?.localizedDescription ?? "")") + appLogger.log("error closing IPC server \(error?.localizedDescription ?? "")") } appLogger.log("(lantern-tunnel) tunnel closed") platformInterface.reset()