From dc8f4a4d1304069c50f8e197f825e7291a3563d3 Mon Sep 17 00:00:00 2001 From: Matt Joiner Date: Tue, 28 May 2024 12:13:19 +1000 Subject: [PATCH 1/2] README: Add app.env requirement and macOS run steps --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9b9a1ddd2..52cb04b1d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Note - you might see an error like `Can't load Kernel binary: Invalid SDK hash.` ### Dependencies -All those dependencies must be in your PATH +All these dependencies must be in your PATH. Some of this is Android specific, see below for other platforms. * Java 11 or greater * [Android Studio](https://developer.android.com/studio?_gl=1*1wowe6v*_up*MQ..&gclid=Cj0KCQjw6auyBhDzARIsALIo6v-bn0juONfkfmQAJtwssRCQWADJMgGfRBisMNTSXHt5CZnyZVSK2Y8aAgCmEALw_wcB&gclsrc=aw.ds) (Android Studio Jellyfish | 2023.3.1 Patch 1) @@ -51,7 +51,8 @@ All those dependencies must be in your PATH * Install all prerequisites * Run `git submodule update --init --recursive` -* Run `git lfs install && git pull` +* Run `git lfs install && git pull`. +* Put the [app.env](https://my.1password.com/vaults/all/allitems/adqasjh2hspgjgvgfllyekhcrq) file from 1Password in the repo root. * Go to the **SDK MANAGER** * Select **Android SDK** * Check the SDK from android 5.0(LOLLIPOP) up to the Latest Version at the moment. @@ -77,7 +78,6 @@ All those dependencies must be in your PATH * `flutter pub get` * `flutter run --flavor prod` - ### 🍏 Running the project on iOS * `make build-framework` (you need to generated Internalsdk.xcframework. containing the Go backend code in order for the project to compile.) @@ -86,18 +86,25 @@ All those dependencies must be in your PATH **Note**: If you're using an M1 or M2 chip, navigate to the ios folder and run `arch -x86_64 pod install` - ### 💻 Running the Project on Desktop **Note**: Make sure to run all the commands from the root of the project. -* Macos run`make darwin` Windows run `make windows` Linux run `linux-amd64` +#### macOS + +* `make darwin` +* `make ffigen` +* `flutter run -d macos` + +#### Other OS + +* Windows run `make windows` Linux run `linux-amd64` * `make ffigen` * `flutter pub get` * `flutter run --flavor prod` or if you are using android studio use desktop configuration - ### Running on emulators + You can easily run emulators directly from the command line with the following: * `flutter devices` @@ -124,7 +131,6 @@ You'll need Ruby >= 2.3 installed and `colorize` gem (i.e., `gem install coloriz ### Building the InternalSdk (AKA Lantern Core) as a library - The core Lantern functionality is written in Go and lives in `./internalsdk`. It is compiled from Go using [Gomobile](https://github.com/golang/mobile) to appropriate formats for each platform. @@ -138,7 +144,6 @@ It is compiled from Go using [Gomobile](https://github.com/golang/mobile) to app For compiled code lives in `./ios/internalsdk/` and is called `Internalsdk.xcframework`. - #### Desktop The desktop app lives under `desktop` .. To build the Go shared library on macOS: From bef437cdab3ef9d28f02d5dc0e849066dba9b8cf Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 28 May 2024 23:18:00 -0700 Subject: [PATCH 2/2] Startup sequence updates on iOS (#1073) * ios updates * ios updates * ios updates to eliminate startup delay * update flashlight, merge latest * update flashlight * uncomment line to setup Lantern service * uncomment line to setup Lantern service * clean-ups, update flashlight * clean-ups, update flashlight * clean-ups * clean-ups * clean-ups * simplify, create reusable functions for running flashlight in internalsdk * simplify, create reusable functions for running flashlight in internalsdk * increase dial timeout on ios to 20 seconds * Add config * Add config * Add config * PacketTunnelProvider update * Add back IosConfigure changes for now for working PacketTunnelProvider * Add back IosConfigure changes for now for working PacketTunnelProvider * clean-ups * fix test case error. * Remove start service method. * further simplify code for starting Lantern on iOS * remove LanternService code * Simplify config fetch code and remove unused code. * use bandit NewWithStats instead of new * Update flashlight * Ability to read stats from tunnel. (#1085) * Ability to read stats from tunnel. * added back stats. * Added comments. * ran swift-format. * use userDefault observer for communication. * Simplify stats methods. * added logs. * Use DispatchSource instead of timer. * Use userid and token to user default. * added more logs. * fixed localisation issue on ios. --------- Co-authored-by: atavism * Hide bandwidth widget and small other changes. * Optimize import. * uncomment line to loadAds * Move invoke call to background. * Move session model methods to background. * check for null on stats tracker. * Update flashlight * run go mod tidy. --------- Co-authored-by: Jigar-f Co-authored-by: jigar-f <132374182+jigar-f@users.noreply.github.com> --- Makefile | 2 +- go.mod | 11 +- go.sum | 11 +- internalsdk/analytics/analytics.go | 2 + internalsdk/android.go | 149 +++--- internalsdk/android_test.go | 2 +- internalsdk/ios/config.go | 429 ++++++++++++++++++ internalsdk/ios/config_test.go | 70 +++ internalsdk/ios/ios.go | 300 ++++++++++++ internalsdk/ios/log.go | 29 ++ internalsdk/ios/logger/logger_darwin.go | 86 ++++ internalsdk/ios/logger/logger_other.go | 12 + internalsdk/ios/memory.go | 185 ++++++++ internalsdk/ios/service.go | 83 ++++ internalsdk/ios/tcp.go | 214 +++++++++ internalsdk/ios/thread_limiting_conn.go | 98 ++++ internalsdk/ios/thread_limiting_conn_test.go | 54 +++ internalsdk/ios/udp.go | 241 ++++++++++ internalsdk/issue.go | 2 +- internalsdk/session_model.go | 57 ++- internalsdk/stats_tracker.go | 4 +- ios/.swiftlint.yml | 6 + ios/Podfile.lock | 127 ++---- ios/Runner.xcodeproj/project.pbxproj | 15 +- .../xcshareddata/xcschemes/prod.xcscheme | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 14 + ios/Runner/AppDelegate.swift | 30 +- ios/Runner/Lantern/Core/Vpn/VPNManager.swift | 13 + ios/Runner/Lantern/Core/Vpn/VpnHelper.swift | 149 ++++-- ios/Runner/Lantern/Models/BaseModel.swift | 20 +- ios/Runner/Lantern/Models/SessionModel.swift | 146 +++--- ios/Runner/Lantern/Models/VpnModel.swift | 1 + ios/Runner/Lantern/Utils/Constants.swift | 7 +- ios/Tunnel/LanternAdapter.swift | 26 ++ ios/Tunnel/PacketTunnelProvider.swift | 2 +- ios/Tunnel/StatsTracker.swift | 37 ++ .../device_linking/approve_device.dart | 1 + lib/account/language.dart | 5 +- lib/account/settings.dart | 5 +- lib/account/support.dart | 1 + lib/app.dart | 2 +- lib/catcher_setup.dart | 1 + lib/common/common.dart | 6 +- lib/common/common_desktop.dart | 12 +- lib/common/ffi_list_subscriber.dart | 4 - lib/common/ffi_subscriber.dart | 6 +- lib/common/model.dart | 4 +- lib/common/tray_handler.dart | 2 - lib/common/ui/app_webview.dart | 1 - lib/common/ui/custom/text.dart | 2 - lib/common/ui/websocket.dart | 3 +- lib/core/router/router.dart | 3 - lib/home.dart | 34 +- lib/i18n/i18n.dart | 2 +- lib/i18n/localization_constants.dart | 7 + lib/messaging/messaging_model.dart | 3 +- .../protos_flutteronly/messaging.pbenum.dart | 1 + .../protos_flutteronly/messaging.pbjson.dart | 2 +- lib/plans/plan_details.dart | 1 - lib/plans/reseller_checkout.dart | 2 +- lib/plans/utils.dart | 1 - lib/replica/logic/uploader.dart | 2 +- lib/vpn/vpn_model.dart | 2 +- lib/vpn/vpn_server_location.dart | 2 + lib/vpn/vpn_status.dart | 1 - lib/vpn/vpn_tab.dart | 9 +- macos/Podfile.lock | 2 +- 68 files changed, 2353 insertions(+), 425 deletions(-) create mode 100644 internalsdk/ios/config.go create mode 100644 internalsdk/ios/config_test.go create mode 100644 internalsdk/ios/ios.go create mode 100644 internalsdk/ios/log.go create mode 100644 internalsdk/ios/logger/logger_darwin.go create mode 100644 internalsdk/ios/logger/logger_other.go create mode 100644 internalsdk/ios/memory.go create mode 100644 internalsdk/ios/service.go create mode 100644 internalsdk/ios/tcp.go create mode 100644 internalsdk/ios/thread_limiting_conn.go create mode 100644 internalsdk/ios/thread_limiting_conn_test.go create mode 100644 internalsdk/ios/udp.go create mode 100644 ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ios/Tunnel/LanternAdapter.swift create mode 100644 ios/Tunnel/StatsTracker.swift diff --git a/Makefile b/Makefile index 16b9a4718..35b443981 100644 --- a/Makefile +++ b/Makefile @@ -674,7 +674,7 @@ build-framework: assert-go-version install-gomobile -tags='headless lantern ios netgo' \ -ldflags="$(LDFLAGS)" \ $(GOMOBILE_EXTRA_BUILD_FLAGS) \ - github.com/getlantern/lantern-client/internalsdk github.com/getlantern/pathdb/testsupport github.com/getlantern/pathdb/minisql github.com/getlantern/flashlight/v7/ios + github.com/getlantern/lantern-client/internalsdk github.com/getlantern/pathdb/testsupport github.com/getlantern/pathdb/minisql github.com/getlantern/lantern-client/internalsdk/ios @echo "moving framework" mkdir -p $(INTERNALSDK_FRAMEWORK_DIR) mv ./$(INTERNALSDK_FRAMEWORK_NAME) $(INTERNALSDK_FRAMEWORK_DIR)/$(INTERNALSDK_FRAMEWORK_NAME) diff --git a/go.mod b/go.mod index d24fef8bd..8684f5255 100644 --- a/go.mod +++ b/go.mod @@ -30,15 +30,20 @@ replace github.com/eycorsican/go-tun2socks => github.com/getlantern/go-tun2socks require ( github.com/blang/semver v3.5.1+incompatible + github.com/dustin/go-humanize v1.0.1 + github.com/eycorsican/go-tun2socks v1.16.12-0.20201107203946-301549c435ff + github.com/fsnotify/fsnotify v1.6.0 github.com/getlantern/appdir v0.0.0-20200615192800-a0ef1968f4da github.com/getlantern/autoupdate v0.0.0-20211217175350-d0b211f39ba7 + github.com/getlantern/common v1.2.1-0.20230427204521-6ac18c21db39 github.com/getlantern/diagnostics v0.0.0-20230503185158-c2fc28ed22fe github.com/getlantern/dnsgrab v0.0.0-20240124035712-497ccf435858 github.com/getlantern/errors v1.0.5-0.20240410211607-f268a297d5d1 github.com/getlantern/eventual v1.0.0 github.com/getlantern/eventual/v2 v2.0.2 github.com/getlantern/filepersist v0.0.0-20210901195658-ed29a1cb0b7c - github.com/getlantern/flashlight/v7 v7.6.78 + github.com/getlantern/flashlight/v7 v7.6.79 + github.com/getlantern/fronted v0.0.0-20230601004823-7fec719639d8 github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 github.com/getlantern/i18n v0.0.0-20181205222232-2afc4f49bb1c github.com/getlantern/idletiming v0.0.0-20231030193830-6767b09f86db @@ -53,6 +58,7 @@ require ( github.com/getlantern/pathdb v0.0.0-20231026090702-54ee1ddd99eb github.com/getlantern/profiling v0.0.0-20160317154340-2a15afbadcff github.com/getlantern/replica v0.14.3 + github.com/getlantern/safechannels v0.0.0-20201218194342-b4e5383e9627 github.com/getlantern/sysproxy v0.0.0-20230319110552-63a8cacb7b9b github.com/getlantern/timezone v0.0.0-20210901200113-3f9de9d360c9 github.com/getlantern/yaml v0.0.0-20190801163808-0c9bb1ebf426 @@ -125,7 +131,6 @@ require ( github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvyukov/go-fuzz v0.0.0-20240203152606-b1ce7bc07150 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/enobufs/go-nats v0.0.1 // indirect @@ -139,7 +144,6 @@ require ( github.com/getlantern/cmux v0.0.0-20230301223233-dac79088a4c0 // indirect github.com/getlantern/cmux/v2 v2.0.0-20230301223233-dac79088a4c0 // indirect github.com/getlantern/cmuxprivate v0.0.0-20231025143958-503c5330c30b // indirect - github.com/getlantern/common v1.2.1-0.20230427204521-6ac18c21db39 // indirect github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect github.com/getlantern/detour v0.0.0-20230503144615-d3106a68f79e // indirect github.com/getlantern/dhtup v0.0.0-20230218071625-e78bcd432e4b // indirect @@ -151,7 +155,6 @@ require ( github.com/getlantern/event v0.0.0-20210901195647-a7e3145142e6 // indirect github.com/getlantern/fdcount v0.0.0-20210503151800-5decd65b3731 // indirect github.com/getlantern/framed v0.0.0-20190601192238-ceb6431eeede // indirect - github.com/getlantern/fronted v0.0.0-20230601004823-7fec719639d8 // indirect github.com/getlantern/geo v0.0.0-20240108161311-50692a1b69a9 // indirect github.com/getlantern/geolookup v0.0.0-20230327091034-aebe73c6eef4 // indirect github.com/getlantern/go-socks5 v0.0.0-20171114193258-79d4dd3e2db5 // indirect diff --git a/go.sum b/go.sum index fc7c3b30e..f155c6607 100644 --- a/go.sum +++ b/go.sum @@ -280,8 +280,8 @@ github.com/getlantern/fdcount v0.0.0-20210503151800-5decd65b3731/go.mod h1:XZwE+ github.com/getlantern/filepersist v0.0.0-20160317154340-c5f0cd24e799/go.mod h1:8DGAx0LNUfXNnEH+fXI0s3OCBA/351kZCiz/8YSK3i8= github.com/getlantern/filepersist v0.0.0-20210901195658-ed29a1cb0b7c h1:mcz27xtAkb1OuOLBct/uFfL1p3XxAIcFct82GbT+UZM= github.com/getlantern/filepersist v0.0.0-20210901195658-ed29a1cb0b7c/go.mod h1:8DGAx0LNUfXNnEH+fXI0s3OCBA/351kZCiz/8YSK3i8= -github.com/getlantern/flashlight/v7 v7.6.78 h1:ADZ/ZJ/UoGQ6g6kGTzrecHt7P3PhOzPLRmsTc1lOzC0= -github.com/getlantern/flashlight/v7 v7.6.78/go.mod h1:GQtpdfigs9k+aoqSdxh2qTdRtAbktJSE5u1Sx3IsgW0= +github.com/getlantern/flashlight/v7 v7.6.79 h1:GAvX9yuCJ3S8qB680gFYM6D/zwmAEk9FcC+LE8hgEGQ= +github.com/getlantern/flashlight/v7 v7.6.79/go.mod h1:1zs3W2WkM5eqJ2fhEsMa8lo6hqmDW8EwokpT7TQ/MrI= github.com/getlantern/framed v0.0.0-20190601192238-ceb6431eeede h1:yrU6Px3ZkvCsDLPryPGi6FN+2iqFPq+JeCb7EFoDBhw= github.com/getlantern/framed v0.0.0-20190601192238-ceb6431eeede/go.mod h1:nhnoiS6DE6zfe+BaCMU4YI01UpsuiXnDqM5S8jxHuuI= github.com/getlantern/fronted v0.0.0-20230601004823-7fec719639d8 h1:r/Z/SPPIfLXDI3QA7/tE6nOfPncrqeUPDjiFjnNGP50= @@ -292,6 +292,8 @@ github.com/getlantern/geolookup v0.0.0-20230327091034-aebe73c6eef4 h1:Ju9l1RretV github.com/getlantern/geolookup v0.0.0-20230327091034-aebe73c6eef4/go.mod h1:4UNvIsawdB8WclVxqYv46Oe1zzWJ8wMhUO+q6tUzATo= github.com/getlantern/go-socks5 v0.0.0-20171114193258-79d4dd3e2db5 h1:RBKofGGMt2k6eGBwX8mky9qunjL+KnAp9JdzXjiRkRw= github.com/getlantern/go-socks5 v0.0.0-20171114193258-79d4dd3e2db5/go.mod h1:kGHRXch95rnGLHjER/GhhFiHvfnqNz7KqWD9kGfATHY= +github.com/getlantern/go-tun2socks v1.16.12-0.20201218023150-b68f09e5ae93 h1:CFLw2b6vgOmpxsRWRiTd46tiR6YKg2crIuTu4cINYcY= +github.com/getlantern/go-tun2socks v1.16.12-0.20201218023150-b68f09e5ae93/go.mod h1:wgB2BFT8ZaPKyKOQ/5dljMG/YIow+AIXyq4KBwJ5sGQ= github.com/getlantern/go-update v0.0.0-20230221120840-8d795213a8bc h1:qZ/HlURAOgGRKtqDGimPwL2w5PkvW7Ap+c1bGRK0pzU= github.com/getlantern/go-update v0.0.0-20230221120840-8d795213a8bc/go.mod h1:DQAFBxfQlSru9Loud3pLM+rF//qf0FQBtB//grF89IA= github.com/getlantern/golog v0.0.0-20190809085441-26e09e6dd330/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= @@ -413,6 +415,8 @@ github.com/getlantern/rot13 v0.0.0-20220822172233-370767b2f782 h1:A1+qM0Dqm0no8A github.com/getlantern/rot13 v0.0.0-20220822172233-370767b2f782/go.mod h1:O0dNqH9hbXlOa9OpVdbACmTBfDPD+ENjbY0cPkzBd9g= github.com/getlantern/rotator v0.0.0-20160829164113-013d4f8e36a2 h1:smFR/kESUKlcdyatoOO3HngBzzrUU6S0LRw2vCBYQPg= github.com/getlantern/rotator v0.0.0-20160829164113-013d4f8e36a2/go.mod h1:Ap+QTDJeA24+0jjPHReq/LyP3ugEEDYvncluEgsm60A= +github.com/getlantern/safechannels v0.0.0-20201218194342-b4e5383e9627 h1:eYXtxjRyiP9f1rMvbnyT4DBWFEiIPY3zzMA5KqVshyc= +github.com/getlantern/safechannels v0.0.0-20201218194342-b4e5383e9627/go.mod h1:QJUudepmTj/KQXnReV6DZSJY/xO05fnfcZf3t8zkNLg= github.com/getlantern/shortcut v0.0.0-20211026183428-bf59a137fdec h1:8TfjIMydnhBs4edmXu2Nz1f2P0QOcXfol2rR1cxfrSs= github.com/getlantern/shortcut v0.0.0-20211026183428-bf59a137fdec/go.mod h1:3VQ6qvEBehqDBNbEKfNtSjK6MR5ydOdjMPgKjKay7vo= github.com/getlantern/sysproxy v0.0.0-20230319110552-63a8cacb7b9b h1:sn68kKTXZ6k7vyhPfYvjtoDSMufTlvvkAZ6Y+VJ25ZA= @@ -862,6 +866,7 @@ github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -1053,6 +1058,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1136,6 +1142,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internalsdk/analytics/analytics.go b/internalsdk/analytics/analytics.go index 0c4bc4572..9f3c33e23 100644 --- a/internalsdk/analytics/analytics.go +++ b/internalsdk/analytics/analytics.go @@ -216,6 +216,8 @@ func getExecutableHash() string { // bad happening here. if common.Platform == "android" { return "android" + } else if common.Platform == "ios" { + return "ios" } if lanternPath, err := os.Executable(); err != nil { log.Debugf("Could not get path to executable %v", err) diff --git a/internalsdk/android.go b/internalsdk/android.go index be35e2b78..8505aeb25 100644 --- a/internalsdk/android.go +++ b/internalsdk/android.go @@ -95,8 +95,8 @@ type Session interface { SerializedInternalHeaders() (string, error) } -// panickingSession wraps the Session interface but panics instead of returning errors -type panickingSession interface { +// PanickingSession wraps the Session interface but panics instead of returning errors +type PanickingSession interface { common.AuthConfig SetCountry(string) UpdateAdSettings(AdSettings) @@ -127,11 +127,15 @@ type panickingSession interface { Wrapped() Session } -// panickingSessionImpl implements panickingSession +// panickingSessionImpl implements PanickingSession type panickingSessionImpl struct { wrapped Session } +func NewPanickingSession(s *SessionModel) PanickingSession { + return &panickingSessionImpl{s} +} + func (s *panickingSessionImpl) Wrapped() Session { return s.wrapped } @@ -274,18 +278,18 @@ func (s *panickingSessionImpl) SerializedInternalHeaders() string { return result } -type userConfig struct { - session panickingSession +type UserConfig struct { + session PanickingSession } -func (uc *userConfig) GetAppName() string { return common.DefaultAppName } -func (uc *userConfig) GetDeviceID() string { return uc.session.GetDeviceID() } -func (uc *userConfig) GetUserID() int64 { return uc.session.GetUserID() } -func (uc *userConfig) GetToken() string { return uc.session.GetToken() } -func (uc *userConfig) GetEnabledExperiments() []string { return nil } -func (uc *userConfig) GetLanguage() string { return uc.session.Locale() } -func (uc *userConfig) GetTimeZone() (string, error) { return uc.session.GetTimeZone(), nil } -func (uc *userConfig) GetInternalHeaders() map[string]string { +func (uc *UserConfig) GetAppName() string { return common.DefaultAppName } +func (uc *UserConfig) GetDeviceID() string { return uc.session.GetDeviceID() } +func (uc *UserConfig) GetUserID() int64 { return uc.session.GetUserID() } +func (uc *UserConfig) GetToken() string { return uc.session.GetToken() } +func (uc *UserConfig) GetEnabledExperiments() []string { return nil } +func (uc *UserConfig) GetLanguage() string { return uc.session.Locale() } +func (uc *UserConfig) GetTimeZone() (string, error) { return uc.session.GetTimeZone(), nil } +func (uc *UserConfig) GetInternalHeaders() map[string]string { h := make(map[string]string) var f interface{} @@ -306,8 +310,8 @@ func (uc *userConfig) GetInternalHeaders() map[string]string { return h } -func newUserConfig(session panickingSession) *userConfig { - return &userConfig{session: session} +func NewUserConfig(session PanickingSession) *UserConfig { + return &UserConfig{session: session} } func getClient(ctx context.Context) *client.Client { @@ -343,8 +347,7 @@ type StartResult struct { DNSGrabAddr string } -// AdSettings is an interface for retrieving mobile ad settings from the -// global config +// AdSettings is an interface for retrieving mobile ad settings from the global config type AdSettings interface { // GetAdProvider gets an ad provider if and only if ads are enabled based on the passed parameters. GetAdProvider(isPro bool, countryCode string, daysSinceInstalled int) (AdProvider, error) @@ -423,7 +426,7 @@ func Start(configDir string, da.(string)}, nil } -func newAnalyticsSession(session panickingSession) analytics.Session { +func newAnalyticsSession(session PanickingSession) analytics.Session { analyticsSession := analytics.Start(session.GetDeviceID(), common.ApplicationVersion) go func() { ipAddress := geolookup.GetIP(forever) @@ -433,13 +436,59 @@ func newAnalyticsSession(session panickingSession) analytics.Session { return analyticsSession } -func run(configDir, locale string, - settings Settings, session panickingSession) { +func InitDnsGrab(configDir string, session PanickingSession) (dnsgrab.Server, error) { + cache, err := persistentcache.New(filepath.Join(configDir, "dnsgrab.cache"), maxDNSGrabAge) + if err != nil { + log.Errorf("unable to open dnsgrab cache: %v", err) + return nil, err + } + grabber, err := dnsgrab.ListenWithCache( + "127.0.0.1:0", + session.GetDNSServer, + cache, + ) + if err != nil { + log.Errorf("unable to start dnsgrab: %v", err) + return nil, err + } + dnsGrabEventual.Set(grabber) + dnsGrabAddrEventual.Set(grabber.LocalAddr().String()) + go func() { + serveErr := grabber.Serve() + if serveErr != nil { + log.Errorf("error serving dns: %v", serveErr) + } + }() + return grabber, nil +} + +func ReverseDns(grabber dnsgrab.Server) func(string) (string, error) { + return func(addr string) (string, error) { + op := ops.Begin("reverse_dns") + defer op.End() + + host, port, splitErr := net.SplitHostPort(addr) + if splitErr != nil { + host = addr + } + ip := net.ParseIP(host) + if ip == nil { + log.Debugf("Unable to parse IP %v, passing through address as is", host) + return addr, nil + } + updatedHost, ok := grabber.ReverseLookup(ip) + if !ok { + return "", op.FailIf(errors.New("unknown IP address %v", ip)) + } + if splitErr != nil { + return updatedHost, nil + } + return fmt.Sprintf("%v:%v", updatedHost, port), nil + } +} + +func run(configDir, locale string, settings Settings, session PanickingSession) { - // memhelper won't build for iOS right now - // memhelper.Track(15*time.Second, 15*time.Second, func(err error) { - // log.Debugf("Unable to track memory stats: %v", err) - // }) appdir.SetHomeDir(configDir) session.SetStaging(false) @@ -465,29 +514,10 @@ func run(configDir, locale string, log.Debugf("Writing log messages to %s/lantern.log", configDir) - cache, err := persistentcache.New(filepath.Join(configDir, "dnsgrab.cache"), maxDNSGrabAge) - if err != nil { - log.Errorf("unable to open dnsgrab cache: %v", err) - return - } - - grabber, err := dnsgrab.ListenWithCache( - "127.0.0.1:0", - session.GetDNSServer, - cache, - ) + grabber, err := InitDnsGrab(configDir, session) if err != nil { - log.Errorf("unable to start dnsgrab: %v", err) return } - dnsGrabEventual.Set(grabber) - dnsGrabAddrEventual.Set(grabber.LocalAddr().String()) - go func() { - serveErr := grabber.Serve() - if serveErr != nil { - log.Errorf("error serving dns: %v", serveErr) - } - }() httpProxyAddr := fmt.Sprintf("%s:%d", settings.GetHttpProxyHost(), @@ -498,7 +528,7 @@ func run(configDir, locale string, config.ForceCountry(forcedCountryCode) } - userConfig := newUserConfig(session) + userConfig := NewUserConfig(session) globalConfigChanged := make(chan interface{}) geoRefreshed := geolookup.OnRefresh() @@ -533,28 +563,7 @@ func run(configDir, locale string, NewStatsTracker(session), session.IsProUser, func() string { return "" }, // only used for desktop - func(addr string) (string, error) { - op := ops.Begin("reverse_dns") - defer op.End() - - host, port, splitErr := net.SplitHostPort(addr) - if splitErr != nil { - host = addr - } - ip := net.ParseIP(host) - if ip == nil { - log.Debugf("Unable to parse IP %v, passing through address as is", host) - return addr, nil - } - updatedHost, ok := grabber.ReverseLookup(ip) - if !ok { - return "", op.FailIf(errors.New("unknown IP address %v", ip)) - } - if splitErr != nil { - return updatedHost, nil - } - return fmt.Sprintf("%v:%v", updatedHost, port), nil - }, + ReverseDns(grabber), func(category, action, label string) {}, ) if err != nil { @@ -619,7 +628,7 @@ func run(configDir, locale string, ) } -func bandwidthUpdates(session panickingSession) { +func bandwidthUpdates(session PanickingSession) { go func() { for quota := range bandwidth.Updates { percent, remaining, allowed := getBandwidth(quota) @@ -650,13 +659,13 @@ func getBandwidth(quota *bandwidth.Quota) (int, int, int) { return percent, remaining, int(quota.MiBAllowed) } -func geoLookup(session panickingSession) { +func geoLookup(session PanickingSession) { country := geolookup.GetCountry(0) log.Debugf("Successful geolookup: country %s", country) session.SetCountry(country) } -func afterStart(session panickingSession) { +func afterStart(session PanickingSession) { bandwidthUpdates(session) go func() { diff --git a/internalsdk/android_test.go b/internalsdk/android_test.go index ad195f240..8dd92db26 100644 --- a/internalsdk/android_test.go +++ b/internalsdk/android_test.go @@ -235,7 +235,7 @@ func TestInternalHeaders(t *testing.T) { } for _, test := range tests { - s := userConfig{&panickingSessionImpl{testSession{serializedInternalHeaders: test.input}}} + s := UserConfig{&panickingSessionImpl{testSession{serializedInternalHeaders: test.input}}} got := s.GetInternalHeaders() assert.Equal(t, test.expected, got, "Headers did not decode as expected") } diff --git a/internalsdk/ios/config.go b/internalsdk/ios/config.go new file mode 100644 index 000000000..d54fab260 --- /dev/null +++ b/internalsdk/ios/config.go @@ -0,0 +1,429 @@ +package ios + +import ( + "bytes" + "compress/gzip" + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httputil" + "os" + "path/filepath" + "time" + + "github.com/getlantern/errors" + "github.com/getlantern/fronted" + "github.com/getlantern/yaml" + + commonconfig "github.com/getlantern/common/config" + "github.com/getlantern/flashlight/v7/config" + "github.com/getlantern/flashlight/v7/email" + "github.com/getlantern/flashlight/v7/embeddedconfig" + "github.com/getlantern/flashlight/v7/geolookup" + "github.com/getlantern/lantern-client/internalsdk/common" +) + +const ( + userConfigYaml = "userconfig.yaml" + globalYaml = "global.yaml" + proxiesYaml = "proxies.yaml" +) + +// ConfigResult captures the result of calling Configure() +type ConfigResult struct { + // VPNNeedsReconfiguring indicates that something in the config changed and + // that the VPN needs to be reconfigured. + VPNNeedsReconfiguring bool + + // IPSToExcludeFromVPN lists all IPS that should be excluded from the VPNS's + // routes in a comma-delimited string + IPSToExcludeFromVPN string +} + +// Configure fetches updated configuration from the cloud and stores it in +// configFolderPath. There are 5 files that must be initialized in +// configFolderPath - global.yaml, global.yaml.etag, proxies.yaml, +// proxies.yaml.etag and masquerade_cache. deviceID should be a string that +// uniquely identifies the current device. hardcodedProxies allows manually specifying +// a proxies.yaml configuration that overrides whatever we fetch from the cloud. +func Configure(configFolderPath string, userID int, proToken, deviceID string, refreshProxies bool, hardcodedProxies string) (*ConfigResult, error) { + log.Debugf("Configuring client for device '%v' at config path '%v' userid '%v' token '%v'", deviceID, configFolderPath, userID, proToken) + defer log.Debug("Finished configuring client") + uc := userConfigFor(userID, proToken, deviceID) + cf := &configurer{ + configFolderPath: configFolderPath, + hardcodedProxies: hardcodedProxies, + uc: uc, + } + return cf.configure(userID, proToken, refreshProxies) +} + +type UserConfig struct { + common.UserConfigData + Country string + AllowProbes bool +} + +type configurer struct { + configFolderPath string + hardcodedProxies string + uc *UserConfig + rt http.RoundTripper +} + +// Important: +// This method is responsible for potentially delaying the UI startup process. +// Occasionally, the execution time of this method varies significantly, sometimes completing within 5 seconds, while other times taking more than 30 seconds. +// For instance, examples from my running +// config.go:167 Configured completed in 35.8970435s +// config.go:167 Configured completed in 4.0234035s +// config.go:176 Configured completed in 3.700574125s + +// TODO: Implement a timeout mechanism to handle prolonged execution times and potentially execute this method in the background to maintain smooth UI startup performance. +func (cf *configurer) configure(userID int, proToken string, refreshProxies bool) (*ConfigResult, error) { + result := &ConfigResult{} + start := time.Now() + if err := cf.writeUserConfig(); err != nil { + return nil, err + } + + global, globalEtag, globalInitialized, err := cf.openGlobal() + if err != nil { + return nil, err + } + + proxies, proxiesEtag, proxiesInitialized, err := cf.openProxies() + if err != nil { + return nil, err + } + + result.VPNNeedsReconfiguring = globalInitialized || proxiesInitialized + + var globalUpdated, proxiesUpdated bool + + setupFronting := func() error { + log.Debug("Setting up fronting") + defer log.Debug("Set up fronting") + if frontingErr := cf.configureFronting(global, shortFrontedAvailableTimeout); frontingErr != nil { + log.Errorf("Unable to configure fronting on first try, update global config directly from GitHub and try again: %v", frontingErr) + global, globalUpdated = cf.updateGlobal(&http.Transport{}, global, globalEtag, "https://raw.githubusercontent.com/getlantern/lantern-binaries/main/cloud.yaml.gz") + return cf.configureFronting(global, longFrontedAvailableTimeout) + } + return nil + } + + if frontingErr := setupFronting(); frontingErr != nil { + log.Errorf("Unable to configure fronting, sticking with embedded configuration: %v", err) + } else { + log.Debug("Refreshing geolookup") + geolookup.Refresh() + + go func() { + cf.uc.Country = geolookup.GetCountry(1 * time.Minute) + log.Debugf("Successful geolookup: country %s", cf.uc.Country) + cf.uc.AllowProbes = global.FeatureEnabled( + config.FeatureProbeProxies, + common.Platform, + cf.uc.AppName, + "", + int64(cf.uc.UserID), + cf.uc.Token != "", + cf.uc.Country) + log.Debugf("Allow probes?: %v", cf.uc.AllowProbes) + if err := cf.writeUserConfig(); err != nil { + log.Errorf("Unable to save updated UserConfig with country and allow probes: %v", err) + } + }() + + log.Debug("Updating global config") + global, globalUpdated = cf.updateGlobal(cf.rt, global, globalEtag, "https://globalconfig.flashlightproxy.com/global.yaml.gz") + log.Debug("Updated global config") + if refreshProxies { + log.Debug("Refreshing proxies") + proxies, proxiesUpdated = cf.updateProxies(proxies, proxiesEtag) + log.Debug("Refreshed proxies") + } + + result.VPNNeedsReconfiguring = result.VPNNeedsReconfiguring || globalUpdated || proxiesUpdated + } + + for _, provider := range global.Client.Fronted.Providers { + for _, masquerade := range provider.Masquerades { + if len(result.IPSToExcludeFromVPN) == 0 { + result.IPSToExcludeFromVPN = masquerade.IpAddress + } else { + result.IPSToExcludeFromVPN = fmt.Sprintf("%v,%v", result.IPSToExcludeFromVPN, masquerade.IpAddress) + } + } + } + + for _, proxy := range proxies { + if proxy.Addr != "" { + host, _, _ := net.SplitHostPort(proxy.Addr) + result.IPSToExcludeFromVPN = fmt.Sprintf("%v,%v", host, result.IPSToExcludeFromVPN) + log.Debugf("Added %v", host) + } + if proxy.MultiplexedAddr != "" { + host, _, _ := net.SplitHostPort(proxy.MultiplexedAddr) + result.IPSToExcludeFromVPN = fmt.Sprintf("%v,%v", host, result.IPSToExcludeFromVPN) + log.Debugf("Added %v", host) + } + } + seconds := time.Since(start).Seconds() + log.Debugf("Configured completed in %v seconds", seconds) + + email.SetDefaultRecipient(global.ReportIssueEmail) + + return result, nil +} + +func (cf *configurer) writeUserConfig() error { + bytes, err := yaml.Marshal(cf.uc) + if err != nil { + return errors.New("Unable to marshal user config: %v", err) + } + if writeErr := ioutil.WriteFile(cf.fullPathTo(userConfigYaml), bytes, 0644); writeErr != nil { + return errors.New("Unable to save userconfig.yaml: %v", err) + } + return nil +} + +func (cf *configurer) readUserConfig() (*UserConfig, error) { + bytes, err := ioutil.ReadFile(cf.fullPathTo(userConfigYaml)) + if err != nil { + return nil, errors.New("Unable to read userconfig.yaml: %v", err) + } + if len(bytes) == 0 { + return nil, errors.New("Empty userconfig.yaml") + } + uc := &UserConfig{} + if parseErr := yaml.Unmarshal(bytes, uc); parseErr != nil { + return nil, errors.New("Unable to parse userconfig.yaml: %v", err) + } + return uc, nil +} + +func (cf *configurer) openGlobal() (*config.Global, string, bool, error) { + cfg := &config.Global{} + etag, updated, err := cf.openConfig(globalYaml, cfg, embeddedconfig.Global) + return cfg, etag, updated, err +} + +func (cf *configurer) openProxies() (map[string]*commonconfig.ProxyConfig, string, bool, error) { + cfg := make(map[string]*commonconfig.ProxyConfig) + etag, updated, err := cf.openConfig(proxiesYaml, cfg, embeddedconfig.Proxies) + return cfg, etag, updated, err +} + +func (cf *configurer) updateFromHardcodedProxies() ([]byte, string, error) { + return []byte(cf.hardcodedProxies), "hardcoded", nil +} + +func (cf *configurer) openConfig(name string, cfg interface{}, embedded []byte) (string, bool, error) { + var initialized bool + configFile := cf.fullPathTo(name) + log.Debugf("Opening config file at %s", configFile) + bytes, err := ioutil.ReadFile(configFile) + if err == nil && len(bytes) > 0 { + log.Debugf("Loaded %v from file", name) + } else { + log.Debugf("Initializing %v from embedded", name) + bytes = embedded + initialized = true + if writeErr := ioutil.WriteFile(configFile, bytes, 0644); writeErr != nil { + return "", false, errors.New("Unable to write embedded %v to disk: %v", name, writeErr) + } + } + if parseErr := yaml.Unmarshal(bytes, cfg); parseErr != nil { + return "", false, errors.New("Unable to parse %v: %v", name, parseErr) + } + etagBytes, err := ioutil.ReadFile(cf.fullPathTo(name + ".etag")) + if err != nil { + log.Debugf("No known etag for %v", name) + etagBytes = []byte{} + } + return string(etagBytes), initialized, nil +} + +func (cf *configurer) updateGlobal(rt http.RoundTripper, cfg *config.Global, etag string, url string) (*config.Global, bool) { + updated := &config.Global{} + didFetch, err := cf.updateFromWeb(rt, globalYaml, etag, updated, url) + if err != nil { + log.Error(err) + } + if didFetch { + cfg = updated + } + return cfg, didFetch +} + +func (cf *configurer) updateProxies(cfg map[string]*commonconfig.ProxyConfig, etag string) (map[string]*commonconfig.ProxyConfig, bool) { + updated := make(map[string]*commonconfig.ProxyConfig) + didFetch, err := cf.updateFromWeb(cf.rt, proxiesYaml, etag, updated, "http://config.getiantem.org/proxies.yaml.gz") + if err != nil { + log.Error(err) + } + if len(updated) == 0 { + log.Error("Proxies returned by config server was empty, ignoring") + didFetch = false + } + if didFetch { + cfg = updated + } + return cfg, didFetch +} + +func (cf *configurer) updateFromWeb(rt http.RoundTripper, name string, etag string, cfg interface{}, url string) (bool, error) { + var bytes []byte + var newETag string + var err error + + if name == proxiesYaml && cf.hardcodedProxies != "" { + bytes, newETag, err = cf.updateFromHardcodedProxies() + } else { + bytes, newETag, err = cf.doUpdateFromWeb(rt, name, etag, cfg, url) + } + if err != nil { + return false, err + } + + if bytes == nil { + // config unchanged + return false, nil + } + + cf.saveConfig(name, bytes) + cf.saveEtag(name, newETag) + + if name == proxiesYaml { + log.Debugf("Updated proxies.yaml from cloud:\n%v", string(bytes)) + } else { + log.Debugf("Updated %v from cloud", name) + } + + return newETag != etag, nil +} + +func (cf *configurer) doUpdateFromWeb(rt http.RoundTripper, name string, etag string, cfg interface{}, url string) ([]byte, string, error) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, "", errors.New("Unable to construct request to fetch %v from %v: %v", name, url, err) + } + + if etag != "" { + req.Header.Set(common.IfNoneMatchHeader, etag) + } + req.Header.Set("Accept", "application/x-gzip") + // Prevents intermediate nodes (domain-fronters) from caching the content + req.Header.Set("Cache-Control", "no-cache") + common.AddCommonHeaders(cf.uc, req) + + // make sure to close the connection after reading the Body + // this prevents the occasional EOFs errors we're seeing with + // successive requests + req.Close = true + + resp, err := rt.RoundTrip(req) + if err != nil { + return nil, "", errors.New("Unable to fetch cloud config at %s: %s", url, err) + } + dump, dumperr := httputil.DumpResponse(resp, false) + if dumperr != nil { + log.Errorf("Could not dump response: %v", dumperr) + } else { + log.Debugf("Response headers from %v:\n%v", url, string(dump)) + } + defer func() { + if closeerr := resp.Body.Close(); closeerr != nil { + log.Errorf("Error closing response body: %v", closeerr) + } + }() + + if resp.StatusCode == 304 { + log.Debugf("%v unchanged in cloud", name) + return nil, "", nil + } else if resp.StatusCode != 200 { + if dumperr != nil { + return nil, "", errors.New("Bad config response code for %v: %v", name, resp.StatusCode) + } + return nil, "", errors.New("Bad config resp for %v:\n%v", name, string(dump)) + } + + newEtag := resp.Header.Get(common.EtagHeader) + buf := &bytes.Buffer{} + body := io.TeeReader(resp.Body, buf) + gzReader, err := gzip.NewReader(body) + if err != nil { + return nil, "", errors.New("Unable to open gzip reader: %s", err) + } + + defer func() { + if err := gzReader.Close(); err != nil { + log.Errorf("Unable to close gzip reader: %v", err) + } + }() + + bytes, err := ioutil.ReadAll(gzReader) + if err != nil { + return nil, "", errors.New("Unable to read response for %v: %v", name, err) + } + + if parseErr := yaml.Unmarshal(bytes, cfg); parseErr != nil { + return nil, "", errors.New("Unable to parse update for %v: %v", name, parseErr) + } + + if newEtag == "" { + sum := md5.Sum(buf.Bytes()) + newEtag = hex.EncodeToString(sum[:]) + } + + return bytes, newEtag, nil +} + +func (cf *configurer) configureFronting(global *config.Global, timeout time.Duration) error { + log.Debug("Configuring fronting") + certs, err := global.TrustedCACerts() + if err != nil { + return errors.New("Unable to read trusted CAs from global config, can't configure domain fronting: %v", err) + } + + fronted.Configure(certs, global.Client.FrontedProviders(), "cloudfront", cf.fullPathTo("masquerade_cache")) + rt, ok := fronted.NewDirect(timeout) + if !ok { + return errors.New("Timed out waiting for fronting to finish configuring") + } + + cf.rt = rt + log.Debug("Configured fronting") + return nil +} + +func (cf *configurer) openFile(filename string) (*os.File, error) { + file, err := os.Open(cf.fullPathTo(filename)) + if err != nil { + err = errors.New("Unable to open %v: %v", filename, err) + } + return file, err +} + +func (cf *configurer) saveConfig(name string, bytes []byte) { + err := ioutil.WriteFile(cf.fullPathTo(name), bytes, 0644) + if err != nil { + log.Errorf("Unable to save config for %v: %v", name, err) + } +} + +func (cf *configurer) saveEtag(name string, etag string) { + err := ioutil.WriteFile(cf.fullPathTo(name+".etag"), []byte(etag), 0644) + if err != nil { + log.Errorf("Unable to save etag for %v: %v", name, err) + } +} + +func (cf *configurer) fullPathTo(filename string) string { + return filepath.Join(cf.configFolderPath, filename) +} diff --git a/internalsdk/ios/config_test.go b/internalsdk/ios/config_test.go new file mode 100644 index 000000000..742883f0e --- /dev/null +++ b/internalsdk/ios/config_test.go @@ -0,0 +1,70 @@ +package ios + +import ( + "os" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/getlantern/flashlight/v7/common" + "github.com/stretchr/testify/require" +) + +const ( + testDeviceID1 = "test1" + testDeviceID2 = "test2" +) + +func TestConfigure(t *testing.T) { + common.CompileTimeApplicationVersion = "8.0.0" + common.LibraryVersion = "8.0.0" + tmpDir, err := os.MkdirTemp("", "config_test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + os.WriteFile(filepath.Join(tmpDir, "global.yaml"), []byte{}, 0644) + os.WriteFile(filepath.Join(tmpDir, "global.yaml.etag"), []byte{}, 0644) + os.WriteFile(filepath.Join(tmpDir, "proxies.yaml"), []byte{}, 0644) + os.WriteFile(filepath.Join(tmpDir, "proxies.yaml.etag"), []byte{}, 0644) + os.WriteFile(filepath.Join(tmpDir, "masquerade_cache"), []byte{}, 0644) + os.WriteFile(filepath.Join(tmpDir, "userconfig.yaml"), []byte{}, 0644) + + const testUserId = 83901 + const testToken = "testToken" + result1, err := Configure(tmpDir, testUserId, testToken, testDeviceID1, true, "") + require.NoError(t, err) + + require.True(t, result1.VPNNeedsReconfiguring) + require.NotEmpty(t, result1.IPSToExcludeFromVPN) + + c := &configurer{configFolderPath: tmpDir} + uc, err := c.readUserConfig() + require.NoError(t, err) + require.Equal(t, testDeviceID1, uc.GetDeviceID()) + + time.Sleep(1 * time.Second) + watcher, err := fsnotify.NewWatcher() + require.NoError(t, err) + err = watcher.Add(c.fullPathTo(userConfigYaml)) + require.NoError(t, err) + result2, err := Configure(tmpDir, testUserId, testToken, testDeviceID2, true, "") + require.NoError(t, err) + ips1 := strings.Split(result1.IPSToExcludeFromVPN, ",") + ips2 := strings.Split(result2.IPSToExcludeFromVPN, ",") + sort.Strings(ips1) + sort.Strings(ips2) + if result2.VPNNeedsReconfiguring { + require.NotEqual(t, ips1, ips2) + } else { + require.Equal(t, ips1, ips2) + } + + // make sure the config file has been changed + <-watcher.Events + uc, err = c.readUserConfig() + require.NoError(t, err) + require.Equal(t, testDeviceID2, uc.GetDeviceID()) +} diff --git a/internalsdk/ios/ios.go b/internalsdk/ios/ios.go new file mode 100644 index 000000000..925a8b7fe --- /dev/null +++ b/internalsdk/ios/ios.go @@ -0,0 +1,300 @@ +package ios + +import ( + "io" + "path/filepath" + "sync" + "time" + + tun2socks "github.com/eycorsican/go-tun2socks/core" + + "github.com/getlantern/common/config" + "github.com/getlantern/dnsgrab" + "github.com/getlantern/dnsgrab/persistentcache" + "github.com/getlantern/errors" + "github.com/getlantern/flashlight/v7/bandit" + "github.com/getlantern/flashlight/v7/chained" + "github.com/getlantern/flashlight/v7/stats" + "github.com/getlantern/ipproxy" + "github.com/getlantern/lantern-client/internalsdk/common" +) + +const ( + maxDNSGrabAge = 1 * time.Hour // this doesn't need to be long because our fake DNS records have a TTL of only 1 second. We use a smaller value than on Android to be conservative with memory usag. + + quotaSaveInterval = 1 * time.Minute + shortFrontedAvailableTimeout = 30 * time.Second + longFrontedAvailableTimeout = 5 * time.Minute + + logMemoryInterval = 5 * time.Second + forceGCInterval = 250 * time.Millisecond + + dialTimeout = 30 * time.Second + shortIdleTimeout = 5 * time.Second + closeTimeout = 1 * time.Second + + maxConcurrentDials = 2 + ipWriteBufferDepth = 100 + downstreamWriteBufferDepth = 100 +) + +type Writer interface { + Write([]byte) bool +} + +type writeRequest struct { + b []byte + ok chan bool +} + +type writerAdapter struct { + writer Writer + requests chan *writeRequest + closeOnce sync.Once +} + +func newWriterAdapter(writer Writer) io.WriteCloser { + wa := &writerAdapter{ + writer: writer, + requests: make(chan *writeRequest, ipWriteBufferDepth), + } + + // MEMORY_OPTIMIZATION - handle all writing of output packets on a single goroutine to avoid creating more native threads + go wa.handleWrites() + return wa +} + +func (wa *writerAdapter) Write(b []byte) (int, error) { + req := &writeRequest{ + b: b, + ok: make(chan bool), + } + wa.requests <- req + ok := <-req.ok + if !ok { + return 0, errors.New("error writing") + } + return len(b), nil +} + +func (wa *writerAdapter) handleWrites() { + for req := range wa.requests { + req.ok <- wa.writer.Write(req.b) + } +} + +func (wa *writerAdapter) Close() error { + wa.closeOnce.Do(func() { + close(wa.requests) + }) + return nil +} + +type ClientWriter interface { + // Write writes the given bytes. As a side effect of writing, we periodically + // record updated bandwidth quota information in the configured quota.txt file. + // If user has exceeded bandwidth allowance, returns a positive integer + // representing the bandwidth allowance. + Write([]byte) (int, error) + + // Reconfigure forces the ClientWriter to update its configuration + Reconfigure() + + Close() error +} + +type StatsTracker interface { + UpdateStats(string, string, string, int, int, bool) +} + +type cw struct { + ipStack io.WriteCloser + client *iosClient + dialer *bandit.BanditDialer + ipp ipproxy.Proxy + quotaTextPath string + lastSavedQuota time.Time +} + +func (c *cw) Write(b []byte) (int, error) { + _, err := c.ipStack.Write(b) + + result := 0 + return result, err +} + +func (c *cw) Reconfigure() { + dialers, err := c.client.loadDialers() + if err != nil { + // this causes the NetworkExtension process to die. Since the VPN is configured as "on-demand", + // the OS will automatically restart the service, at which point we'll read the new config anyway. + panic(log.Errorf("Unable to load dialers on reconfigure: %v", err)) + } + + c.dialer, err = bandit.New(dialers) + if err != nil { + log.Errorf("Unable to create dialer on reconfigure: %v", err) + } +} + +func (c *cw) Close() error { + if c.client != nil { + c.client.packetsOut.Close() + } + return nil +} + +type iosClient struct { + packetsOut io.WriteCloser + udpDialer UDPDialer + + memChecker MemChecker + configDir string + ipp ipproxy.Proxy + mtu int + capturedDNSHost string + realDNSHost string + uc common.UserConfig + tcpHandler *proxiedTCPHandler + udpHandler *directUDPHandler + + clientWriter *cw + memoryAvailable int64 + started time.Time + statsTracker StatsTracker +} + +func Client(packetsOut Writer, udpDialer UDPDialer, memChecker MemChecker, configDir string, mtu int, capturedDNSHost, realDNSHost string, statsTracker StatsTracker) (ClientWriter, error) { + log.Debug("Creating new iOS client") + if mtu <= 0 { + log.Debug("Defaulting MTU to 1500") + mtu = 1500 + } + + c := &iosClient{ + packetsOut: newWriterAdapter(packetsOut), + memChecker: memChecker, + configDir: configDir, + //ipp: ipp, + mtu: mtu, + udpDialer: udpDialer, + capturedDNSHost: capturedDNSHost, + realDNSHost: realDNSHost, + started: time.Now(), + statsTracker: statsTracker, + } + optimizeMemoryUsage(&c.memoryAvailable) + go c.gcPeriodically() + go c.logMemory() + + return c.start() +} + +func (c *iosClient) start() (ClientWriter, error) { + if err := c.loadUserConfig(); err != nil { + return nil, log.Errorf("error loading user config: %v", err) + } + log.Debugf("Running client at config path '%v'", c.configDir) + start := time.Now() + log.Debugf("User config process start at %v", start) + dialers, err := c.loadDialers() + if err != nil { + return nil, err + } + if len(dialers) == 0 { + return nil, errors.New("No dialers found") + } + tracker := stats.NewTracker() + dialer, err := bandit.NewWithStats(dialers, tracker) + if err != nil { + return nil, err + } + go func() { + tracker.AddListener(func(st stats.Stats) { + if st.City != "" && st.Country != "" && st.CountryCode != "" { + start := time.Now() + log.Debugf("Stats update at %v", start) + c.statsTracker.UpdateStats(st.City, st.Country, st.CountryCode, st.HTTPSUpgrades, st.AdsBlocked, st.HasSucceedingProxy) + } + }) + }() + + // We use a persistent cache for dnsgrab because some clients seem to hang on to our fake IP addresses for a while, even though we set a TTL of 1 second. + // That can be a problem when the network extension is automatically restarted. Caching the dns cache on disk allows us to successfully reverse look up + // those IP addresses even after a restart. + cacheFile := filepath.Join(c.configDir, "dnsgrab.cache") + cache, err := persistentcache.New(cacheFile, maxDNSGrabAge) + if err != nil { + return nil, errors.New("Unable to initialize dnsgrab cache at %v: %v", cacheFile, err) + } + + grabber, err := dnsgrab.ListenWithCache( + "127.0.0.1:0", + func() string { return c.realDNSHost }, + cache, + ) + if err != nil { + return nil, errors.New("Unable to start dnsgrab: %v", err) + } + + c.tcpHandler = newProxiedTCPHandler(c, dialer, grabber) + c.udpHandler = newDirectUDPHandler(c, c.udpDialer, grabber, c.capturedDNSHost) + + ipStack := tun2socks.NewLWIPStack() + tun2socks.RegisterOutputFn(c.packetsOut.Write) + tun2socks.RegisterTCPConnHandler(c.tcpHandler) + tun2socks.RegisterUDPConnHandler(c.udpHandler) + + freeMemory() + + c.clientWriter = &cw{ + ipStack: ipStack, + client: c, + quotaTextPath: filepath.Join(c.configDir, "quota.txt"), + } + + return c.clientWriter, nil +} + +func (c *iosClient) loadUserConfig() error { + cf := &configurer{configFolderPath: c.configDir} + uc, err := cf.readUserConfig() + if err != nil { + return err + } + c.uc = uc + return nil +} + +func (c *iosClient) loadDialers() ([]bandit.Dialer, error) { + cf := &configurer{configFolderPath: c.configDir} + chained.PersistSessionStates(c.configDir) + + proxies := make(map[string]*config.ProxyConfig) + _, _, err := cf.openConfig(proxiesYaml, proxies, []byte{}) + if err != nil { + return nil, err + } + + dialers := chained.CreateDialers(c.configDir, proxies, c.uc) + chained.TrackStatsFor(dialers, c.configDir) + return dialers, nil +} + +func partialUserConfigFor(deviceID string) *UserConfig { + return userConfigFor(0, "", deviceID) +} + +func userConfigFor(userID int, proToken, deviceID string) *UserConfig { + // TODO: plug in implementation of fetching timezone for iOS to work around https://github.com/golang/go/issues/20455 + return &UserConfig{ + UserConfigData: *common.NewUserConfig( + "Lantern", + deviceID, + int64(userID), + proToken, + nil, // Headers currently unused + "", // Language currently unused + ), + } +} diff --git a/internalsdk/ios/log.go b/internalsdk/ios/log.go new file mode 100644 index 000000000..e059e0c3e --- /dev/null +++ b/internalsdk/ios/log.go @@ -0,0 +1,29 @@ +package ios + +import ( + "github.com/getlantern/golog" + "github.com/getlantern/lantern-client/internalsdk/ios/logger" +) + +var ( + log = golog.LoggerFor("ios") + statsLog = golog.LoggerFor("ios.stats") + swiftLog = golog.LoggerFor("ios.swift") +) + +// ConfigureFileLogging configures logging to log to files at the given fullLogFilePath +// and capture heap and goroutine profiles at the given profile path. +func ConfigureFileLogging(fullLogFilePath string, profilePath string) error { + SetProfilePath(profilePath) + return logger.ConfigureFileLogging(fullLogFilePath) +} + +// LogDebug logs the given msg to the swift logger at debug level +func LogDebug(msg string) { + swiftLog.Debug(msg) +} + +// LogError logs the given msg to the swift logger at error level +func LogError(msg string) { + swiftLog.Error(msg) +} diff --git a/internalsdk/ios/logger/logger_darwin.go b/internalsdk/ios/logger/logger_darwin.go new file mode 100644 index 000000000..2437b0608 --- /dev/null +++ b/internalsdk/ios/logger/logger_darwin.go @@ -0,0 +1,86 @@ +package logger + +// #include +// +// void log_debug(const char *msg) +// { +// os_log_debug(OS_LOG_DEFAULT, "%{public}s", msg); +// } +// +// void log_error(const char *msg) +// { +// os_log_error(OS_LOG_DEFAULT, "%{public}s", msg); +// } +import "C" + +import ( + "io" + "os" + "path/filepath" + "strings" + + "github.com/getlantern/flashlight/v7/logging" + "github.com/getlantern/golog" +) + +func init() { + golog.SetOutputs(defaultLoggers()) +} + +// ConfigureFileLogging configures file logging to use the lantern.log file at +// the given path. It is required that the file, as well as files lantern.log.1 +// through lantern.log.5 already exist so that they are writeable from the Go +// side. +func ConfigureFileLogging(fullLogFilePath string) error { + logFileDirectory, filename := filepath.Split(fullLogFilePath) + appName := strings.Split(filename, ".")[0] + werr, wout := defaultLoggers() + return logging.EnableFileLoggingWith(werr, wout, appName, logFileDirectory, 10, 10) +} + +func defaultLoggers() (io.WriteCloser, io.WriteCloser) { + return &dualWriter{os.Stderr, loggerWith(func(msg string) { + C.log_error(C.CString(msg)) + })}, + &dualWriter{os.Stdout, loggerWith(func(msg string) { + C.log_debug(C.CString(msg)) + })} +} + +type dualWriter struct { + w1 io.Writer + w2 io.WriteCloser +} + +func (dw *dualWriter) Write(b []byte) (int, error) { + n, err := dw.w1.Write(b) + n2, err2 := dw.w2.Write(b) + if n2 < n { + n = n2 + } + if err == nil { + err = err2 + } + return n, err +} + +func (dw *dualWriter) Close() error { + return dw.w2.Close() +} + +func loggerWith(fn func(string)) io.WriteCloser { + return &loggerFn{fn} +} + +type loggerFn struct { + log func(string) +} + +func (lf *loggerFn) Write(msg []byte) (int, error) { + lf.log(string(msg)) + return len(msg), nil +} + +func (lf *loggerFn) Close() error { + return nil +} diff --git a/internalsdk/ios/logger/logger_other.go b/internalsdk/ios/logger/logger_other.go new file mode 100644 index 000000000..647d4e8f6 --- /dev/null +++ b/internalsdk/ios/logger/logger_other.go @@ -0,0 +1,12 @@ +//go:build !windows && !darwin +// +build !windows,!darwin + +package logger + +import ( + "github.com/getlantern/errors" +) + +func ConfigureFileLogging(fullLogFilePath string) error { + return errors.New("ConfigureFileLogging is only supported on darwin") +} diff --git a/internalsdk/ios/memory.go b/internalsdk/ios/memory.go new file mode 100644 index 000000000..81fb7ba67 --- /dev/null +++ b/internalsdk/ios/memory.go @@ -0,0 +1,185 @@ +package ios + +import ( + "context" + "net" + "os" + "path/filepath" + "runtime" + "runtime/debug" + "runtime/pprof" + "sync" + "sync/atomic" + "time" + + "github.com/dustin/go-humanize" + "github.com/getlantern/flashlight/v7/chained" + "github.com/getlantern/netx" +) + +// Memory management on iOS is critical because we're running in a network extension that's limited to 15 MB of memory. We handle this using several techniques. +// +// All places in the code that do odd stuff in order to help with memory optimization are marked with MEMORY_OPTIMIZATION +// +// 1. Limit the number of goroutines that write to/from lwip in order to limit the number of OS threads (each OS thread has 0.5MB of stack allocated to it, which gets big pretty quickly) +// 2. Limit concurrency for dialing upstream in order to limit the memory involved with public key cryptography +// 3. Use a fork of go-tun2socks tuned for low memory usage (see https://lwip.fandom.com/wiki/Tuning_TCP and https://lwip.fandom.com/wiki/Lwipopts.h) +// 4. Set an aggressive GCPercent +// 5. Use Go 1.14 instead of 1.15 (seems to have lower memory usage for some reason) +// 6. Use short idle timeouts to reduce the number of simultaneously open connections +// 7. Use small send and receive buffers for upstream TCP connections, adapting to the available amount of system memory + +var ( + profilePath string + profilePathMx sync.RWMutex +) + +func SetProfilePath(path string) { + profilePathMx.Lock() + defer profilePathMx.Unlock() + profilePath = path +} + +func getProfilePath() string { + profilePathMx.Lock() + defer profilePathMx.Unlock() + return profilePath +} + +// MemChecker checks the system's memory level +type MemChecker interface { + // BytesRemain returns the number of bytes of memory left before we hit the system limit + BytesRemain() int +} + +func optimizeMemoryUsage(memoryAvailable *int64) { + // MEMORY_OPTIMIZATION - limit the number of CPUs used to reduce the number of OS threads (and associated stack) to keep memory usage down + runtime.GOMAXPROCS(1) + + // MEMORY_OPTIMIZATION - set very aggressive IdleTimeout to help deal with memory constraints on iOS + chained.IdleTimeout = shortIdleTimeout + + // MEMORY_OPTIMIZATION - set an aggressive target for triggering GC after new allocations reach 20% of heap + debug.SetGCPercent(20) + + var dialer net.Dialer + netx.OverrideDial(func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := dialer.DialContext(ctx, network, addr) + if err == nil { + tcpConn, ok := conn.(*net.TCPConn) + if ok { + // MEMORY_OPTIMIZATION - set small send and receive buffers for cases where we have lots of connections and a flaky network + // This can reduce throughput, especially on networks with high packet loss. + bytesRemain := int(atomic.LoadInt64(memoryAvailable)) + bufferSize := bytesRemain / 25 // this factor gives us a buffer size of about 80KB when remaining memory is about 2MB. + if bufferSize < 4096 { + // never go smaller than 4096 + bufferSize = 4096 + } + tcpConn.SetWriteBuffer(bufferSize) + tcpConn.SetReadBuffer(bufferSize) + } + } + return conn, err + }) +} + +func (c *iosClient) logMemory() { + for { + c.doLogMemory() + time.Sleep(logMemoryInterval) + } +} + +func (c *iosClient) gcPeriodically() { + ticker := time.NewTicker(forceGCInterval) + for range ticker.C { + // this select ensures that if ticker fired while we were checking memory (i.e. it took longer than forceGCInterval), we wait until the ticket fires again to check memory + select { + case <-ticker.C: + continue + default: + freeMemory() + atomic.StoreInt64(&c.memoryAvailable, int64(c.memChecker.BytesRemain())) + } + } +} + +func (c *iosClient) doLogMemory() { + bytesRemain := atomic.LoadInt64(&c.memoryAvailable) + if bytesRemain < 0 { + bytesRemain = 0 + } + + memstats := &runtime.MemStats{} + runtime.ReadMemStats(memstats) + + numOSThreads, _ := runtime.ThreadCreateProfile(nil) + log.Debugf("Memory System Bytes Remain: %v Num OS Threads: %d Go InUse: %v", + humanize.Bytes(uint64(bytesRemain)), + numOSThreads, + humanize.Bytes(memstats.HeapInuse)) + + stats := debug.GCStats{ + PauseQuantiles: make([]time.Duration, 10), + } + debug.ReadGCStats(&stats) + + elapsed := time.Now().Sub(c.started) + log.Debugf("Memory GC num: %v total pauses: %v (%.2f%%) pause percentiles: %v", stats.NumGC, stats.PauseTotal, float64(stats.PauseTotal)*100/float64(elapsed), stats.PauseQuantiles) +} + +func freeMemory() { + debug.FreeOSMemory() // this calls garbage collection before freeing memory to the OS +} + +func captureProfiles() { + log.Debug("Capturing profiles") + + // always free memory before capturing profiles because we need at least one GC before capturing heap data to get appropriate stats + freeMemory() + + path := getProfilePath() + if path == "" { + log.Error("No profile path set, can't capture profiles") + return + } + + heap, err := os.OpenFile(filepath.Join(path, "heap.profile.tmp"), os.O_TRUNC|os.O_CREATE|os.O_RDWR|os.O_SYNC, 0644) + if err != nil { + log.Errorf("Unable to open heap profile file %v for writing: %v", path, err) + return + } + defer heap.Close() + + goroutine, err := os.OpenFile(filepath.Join(path, "goroutine_profile.txt.tmp"), os.O_TRUNC|os.O_CREATE|os.O_RDWR|os.O_SYNC, 0644) + if err != nil { + log.Errorf("Unable to open heap profile file %v for writing: %v", path, err) + return + } + defer goroutine.Close() + + err = pprof.WriteHeapProfile(heap) + if err != nil { + log.Errorf("Unable to capture heap profile: %v", err) + } else { + err = os.Rename(filepath.Join(path, "heap.profile.tmp"), filepath.Join(path, "heap.profile")) + if err != nil { + log.Errorf("Unable to rename heap profile: %v", err) + } else { + log.Debugf("Captured heap profile") + } + } + + err = pprof.Lookup("goroutine").WriteTo(goroutine, 1) + if err != nil { + log.Errorf("Unable to capture goroutine profile: %v", err) + } else { + err = os.Rename(filepath.Join(path, "goroutine_profile.txt.tmp"), filepath.Join(path, "goroutine_profile.txt")) + if err != nil { + log.Errorf("Unable to rename goroutine profile: %v", err) + } else { + log.Debugf("Captured goroutine profile") + } + } +} diff --git a/internalsdk/ios/service.go b/internalsdk/ios/service.go new file mode 100644 index 000000000..e793a5fd7 --- /dev/null +++ b/internalsdk/ios/service.go @@ -0,0 +1,83 @@ +package ios + +// var ( +// clEventual = eventual.NewValue() +// startOnce sync.Once +// ) + +// type LanternService struct { +// memoryAvailable int64 +// sessionModel *internalsdk.SessionModel +// } + +// func newService(sessionModel *internalsdk.SessionModel) *LanternService { +// return &LanternService{ +// sessionModel: sessionModel, +// } +// } + +// func (s *LanternService) Start(configDir string, locale string, settings internalsdk.Settings) { +// optimizeMemoryUsage(&s.memoryAvailable) +// logging.EnableFileLogging(common.DefaultAppName, filepath.Join(configDir, "logs")) +// session := internalsdk.NewPanickingSession(s.sessionModel) +// startOnce.Do(func() { +// go run(configDir, locale, settings, session) +// }) +// } + +// func run(configDir, locale string, settings internalsdk.Settings, session internalsdk.PanickingSession) { +// log.Debugf("Starting lantern: configDir %s locale %s sticky config %t", +// configDir, locale, settings.StickyConfig()) + +// // Set home directory prior to starting Lantern +// appdir.SetHomeDir(configDir) + +// _, err := flashlight.New( +// common.DefaultAppName, +// common.ApplicationVersion, +// common.RevisionDate, +// configDir, // place to store lantern configuration +// false, // don't enable vpn mode for iOS (VPN is handled in the Swift layer) +// func() bool { return false }, // always connected +// func() bool { return true }, +// func() bool { return false }, // do not proxy private hosts on iOS +// func() bool { return true }, // auto report +// map[string]interface{}{}, +// func(cfg *config.Global, src config.Source) { +// b, err := yaml.Marshal(cfg) +// if err != nil { +// log.Errorf("Unable to marshal user config: %v", err) +// } else { +// log.Debugf("Got new global config %s", string(b)) +// } +// }, // onConfigUpdate +// func(proxies []bandit.Dialer, src config.Source) { + +// }, // onProxiesUpdate +// internalsdk.NewUserConfig(session), +// internalsdk.NewStatsTracker(session), +// session.IsProUser, +// func() string { return "" }, // only used for desktop +// func(addr string) (string, error) { +// return "", nil +// }, +// func(category, action, label string) {}, +// ) +// if err != nil { +// log.Fatalf("failed to create new instance of flashlight: %v", err) +// } +// } + +// func HTTPProxyPort() int { +// if addr, ok := client.Addr(6 * time.Second); ok { +// if _, p, err := net.SplitHostPort(addr.(string)); err == nil { +// port, _ := strconv.Atoi(p) +// return port +// } +// } +// log.Errorf("Couldn't retrieve HTTP proxy addr in time") +// return 0 +// } +func main() { + +} diff --git a/internalsdk/ios/tcp.go b/internalsdk/ios/tcp.go new file mode 100644 index 000000000..44cfd9f5f --- /dev/null +++ b/internalsdk/ios/tcp.go @@ -0,0 +1,214 @@ +package ios + +import ( + "context" + "fmt" + "io" + "net" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/getlantern/dnsgrab" + "github.com/getlantern/flashlight/v7/bandit" + "github.com/getlantern/idletiming" + "github.com/getlantern/netx" +) + +type dialRequest struct { + ctx context.Context + addr string + upstream chan net.Conn + err chan error +} + +// proxiedTCPHandler implements TCPConnHandler from go-tun2socks by routing TCP connections +// via our proxies. +type proxiedTCPHandler struct { + dialOut func(ctx context.Context, network, addr string) (net.Conn, error) + client *iosClient + grabber dnsgrab.Server + mtu int + dialRequests chan *dialRequest + downstreamWriteWorker *worker + upstreams map[io.Closer]io.Closer + dialingConns int64 + copyingConns int64 + mx sync.RWMutex +} + +func newProxiedTCPHandler(c *iosClient, dialer *bandit.BanditDialer, grabber dnsgrab.Server) *proxiedTCPHandler { + result := &proxiedTCPHandler{ + dialOut: dialer.DialContext, + client: c, + grabber: grabber, + mtu: c.mtu, + dialRequests: make(chan *dialRequest), + downstreamWriteWorker: newWorker(downstreamWriteBufferDepth), + upstreams: make(map[io.Closer]io.Closer), + } + go result.trackStats() + result.handleDials() + return result +} + +// dialing is very memory intensive because of the cryptography involved, so we limit the concurrency of dialing to keep our memory usage +// under control +func (h *proxiedTCPHandler) handleDials() { + for i := 0; i < maxConcurrentDials; i++ { + go h.handleDial() + } +} + +func (h *proxiedTCPHandler) handleDial() { + // MEMORY_OPTIMIZATION - locking to the OS thread seems to help keep Go from spawning more OS threads when cgo calls are blocked + runtime.LockOSThread() + + for req := range h.dialRequests { + upstream, err := h.dialOut(req.ctx, bandit.NetworkConnect, req.addr) + if err == nil { + req.upstream <- upstream + } else { + req.err <- err + } + } +} + +func (h *proxiedTCPHandler) Handle(_downstream net.Conn, addr *net.TCPAddr) error { + host, ok := h.grabber.ReverseLookup(addr.IP) + if !ok { + return log.Errorf("Invalid ip address %v, will not connect", addr.IP) + } + + ctx, cancelContext := context.WithTimeout(context.Background(), dialTimeout) + addrString := fmt.Sprintf("%v:%d", host, addr.Port) + + // MEMORY_OPTIMIZATION - dialing is very memory intensive because of the cryptography involved, so we limit the + // concurrency of dialing to keep our memory usage under control + req := &dialRequest{ + ctx: ctx, + addr: addrString, + upstream: make(chan net.Conn), + err: make(chan error), + } + var upstream net.Conn + var err error + + atomic.AddInt64(&h.dialingConns, 1) + h.dialRequests <- req + + select { + case upstream = <-req.upstream: + // okay + case err = <-req.err: + // error + } + atomic.AddInt64(&h.dialingConns, -1) + + if err != nil { + cancelContext() + return log.Errorf("Unable to dial %v: %v", addrString, err) + } + + downstream := newThreadLimitingTCPConn(_downstream, h.downstreamWriteWorker) + + h.mx.Lock() + h.upstreams[downstream] = upstream + h.mx.Unlock() + + go func() { + h.copy(downstream, upstream) + cancelContext() + }() + + return nil +} + +func (h *proxiedTCPHandler) copy(downstream net.Conn, upstream net.Conn) { + atomic.AddInt64(&h.copyingConns, 1) + defer atomic.AddInt64(&h.copyingConns, -1) + + defer downstream.Close() + defer upstream.Close() + defer func() { + h.mx.Lock() + delete(h.upstreams, downstream) + h.mx.Unlock() + }() + + // MEMORY_OPTIMIZATION - we don't pool these as pooling seems to create additional memory pressure somehow + bufOut := make([]byte, h.mtu) + bufIn := make([]byte, h.mtu) + + closeTimer := time.NewTimer(closeTimeout) + keepFresh := func(_ int) { + if !closeTimer.Stop() { + <-closeTimer.C + } + closeTimer.Reset(closeTimeout) + } + + // MEMORY_OPTIMIZATION - use a threadLimitingTCPConn to limit the number of goroutines that write to lwip + outErrCh, inErrCh := netx.BidiCopyWithOpts(upstream, downstream, &netx.CopyOpts{ + BufOut: bufOut, + BufIn: bufIn, + OnOut: keepFresh, + OnIn: keepFresh, + }) + + logError := func(err error) { + if err == nil { + return + } else if idletiming.IsIdled(upstream) { + log.Debug(err) + } else { + log.Error(err) + } + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + + outErr := <-outErrCh + logError(outErr) + select { + case inErr := <-inErrCh: + logError(inErr) + case <-closeTimer.C: + log.Trace("opposite direction idle for more than closeTimeout, close everything") + upstream.Close() + downstream.Close() + } + }() + + go func() { + defer wg.Done() + + inErr := <-inErrCh + logError(inErr) + select { + case outErr := <-outErrCh: + logError(outErr) + case <-closeTimer.C: + log.Trace("opposite direction idle for more than closeTimeout, close everything") + upstream.Close() + downstream.Close() + } + }() + + wg.Wait() +} + +func (h *proxiedTCPHandler) trackStats() { + for { + h.mx.RLock() + numUpstreams := len(h.upstreams) + h.mx.RUnlock() + statsLog.Debugf("TCP Conns Upstreams: %d Dialing: %d Copying: %d", numUpstreams, atomic.LoadInt64(&h.dialingConns), atomic.LoadInt64(&h.copyingConns)) + time.Sleep(1 * time.Second) + } +} diff --git a/internalsdk/ios/thread_limiting_conn.go b/internalsdk/ios/thread_limiting_conn.go new file mode 100644 index 000000000..09c58626c --- /dev/null +++ b/internalsdk/ios/thread_limiting_conn.go @@ -0,0 +1,98 @@ +package ios + +import ( + "errors" + "net" + "runtime" + "sync" + + "github.com/eycorsican/go-tun2socks/core" + + "github.com/getlantern/safechannels" +) + +var ( + errWriteOnClosedConn = errors.New("write on closed threadLimitingTCPConn") +) + +type worker struct { + tasks chan func() +} + +func newWorker(bufferDepth int) *worker { + w := &worker{ + tasks: make(chan func(), bufferDepth), + } + go w.work() + return w +} + +func (w *worker) work() { + // MEMORY_OPTIMIZATION - locking to the OS thread seems to help keep Go from spawning more OS threads when cgo calls are blocked + runtime.LockOSThread() + + for task := range w.tasks { + task() + } +} + +type threadLimitingTCPConn struct { + net.Conn + writeWorker *worker + writeResult safechannels.IO + closeOnce sync.Once +} + +func newThreadLimitingTCPConn(wrapped net.Conn, writeWorker *worker) net.Conn { + return &threadLimitingTCPConn{ + Conn: wrapped, + writeWorker: writeWorker, + writeResult: safechannels.NewIO(1), + } +} + +func (c *threadLimitingTCPConn) Write(b []byte) (int, error) { + c.writeWorker.tasks <- func() { + c.writeResult.Write(c.Conn.Write(b)) + } + result, ok := <-c.writeResult.Read() + if !ok { + return 0, errWriteOnClosedConn + } + return result.N, result.Err +} + +func (c *threadLimitingTCPConn) Close() (err error) { + c.closeOnce.Do(func() { + c.writeResult.Close() + err = c.Conn.Close() + }) + return +} + +// Wrapped implements the interface netx.WrappedConn +func (c *threadLimitingTCPConn) Wrapped() net.Conn { + return c.Conn +} + +type threadLimitingUDPConn struct { + core.UDPConn + writeWorker *worker +} + +func newThreadLimitingUDPConn(wrapped core.UDPConn, writeWorker *worker) core.UDPConn { + return &threadLimitingUDPConn{ + UDPConn: wrapped, + writeWorker: writeWorker, + } +} + +func (c *threadLimitingUDPConn) WriteFrom(b []byte, addr *net.UDPAddr) (int, error) { + writeResult := make(chan *safechannels.IOResult) + c.writeWorker.tasks <- func() { + n, err := c.UDPConn.WriteFrom(b, addr) + writeResult <- &safechannels.IOResult{n, err} + } + result := <-writeResult + return result.N, result.Err +} diff --git a/internalsdk/ios/thread_limiting_conn_test.go b/internalsdk/ios/thread_limiting_conn_test.go new file mode 100644 index 000000000..ef78b475e --- /dev/null +++ b/internalsdk/ios/thread_limiting_conn_test.go @@ -0,0 +1,54 @@ +package ios + +import ( + "io" + "net" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestThreadLimitingConn(t *testing.T) { + const msg = "echo me" + + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + go func() { + for { + conn, err := l.Accept() + if err == nil { + go func() { + io.Copy(conn, conn) + }() + } + } + }() + + worker := newWorker(1) + + readers := 100 + var wg sync.WaitGroup + wg.Add(readers) + for i := 0; i < readers; i++ { + go func() { + defer wg.Done() + _conn, err := net.Dial("tcp", l.Addr().String()) + require.NoError(t, err) + conn := newThreadLimitingTCPConn(_conn, worker) + n, err := conn.Write([]byte(msg)) + require.Equal(t, n, len(msg)) + b := make([]byte, 1024) + n, err = conn.Read(b) + require.Equal(t, n, len(msg)) + require.Equal(t, msg, string(b[:n])) + + conn.Close() + _, err = conn.Write([]byte("extra")) + require.Equal(t, errWriteOnClosedConn, err) + }() + } + + wg.Wait() +} diff --git a/internalsdk/ios/udp.go b/internalsdk/ios/udp.go new file mode 100644 index 000000000..efadd5049 --- /dev/null +++ b/internalsdk/ios/udp.go @@ -0,0 +1,241 @@ +package ios + +import ( + "net" + "sync" + "sync/atomic" + "time" + + "github.com/eycorsican/go-tun2socks/core" + + "github.com/getlantern/dnsgrab" + "github.com/getlantern/flashlight/v7/chained" +) + +// UDPDialer provides a mechanism for dialing outbound UDP connections that bypass the VPN. +// The returned UDPConn is not immediately ready for use, only once the UDPCallbacks receive +// OnDialSuccess is the UDPConn ready for use. +type UDPDialer interface { + Dial(host string, port int) UDPConn +} + +// UDPConn is a UDP connection that bypasses the VPN. It is backed by an NWConnection on the +// Swift side. +// +// See https://developer.apple.com/documentation/network/nwconnection. +type UDPConn interface { + // RegisterCallbacks registers lifecycle callbacks for the connection. Clients of the UDPConn + // must call this before trying to use WriteDatagram and ReceiveDatagram. + RegisterCallbacks(cb *UDPCallbacks) + + // WriteDatagram writes one datagram to the UDPConn. Any resulting error from the right will + // be reported to UDPCallbacks.OnError. + WriteDatagram([]byte) + + // ReceiveDatagram requests receipt of the next datagram from the UDPConn. Once the datagram is received, + // it's sent to UDPCallbacks.OnReceive. + ReceiveDatagram() + + // Close closes the UDPConn + Close() +} + +type UDPCallbacks struct { + h *directUDPHandler + downstream core.UDPConn + upstream UDPConn + target *net.UDPAddr + dialSucceeded chan interface{} + dialFailed chan interface{} + received chan interface{} + wrote chan interface{} +} + +// OnConn is called once a connection is successfully dialed +func (cb *UDPCallbacks) OnDialSucceeded() { + close(cb.dialSucceeded) +} + +func (cb *UDPCallbacks) OnError(err error) { + log.Errorf("Error communicating with %v: %v", cb.target, err) +} + +// OnClose is called when the connection is closed. +func (cb *UDPCallbacks) OnClose() { + cb.h.Lock() + delete(cb.h.upstreams, cb.downstream) + cb.h.Unlock() + cb.downstream.Close() +} + +func (cb *UDPCallbacks) OnReceive(dgram []byte) { + // Request receive of next datagram + cb.upstream.ReceiveDatagram() + + // Forward datagram downstream + _, writeErr := cb.downstream.WriteFrom(dgram, cb.target) + if writeErr != nil { + log.Errorf("Unable to write UDP packet downstream: %v", writeErr) + cb.upstream.Close() + } + + cb.received <- nil +} + +func (cb *UDPCallbacks) OnWritten() { + cb.wrote <- nil +} + +func (cb *UDPCallbacks) idleTiming() { + t := time.NewTimer(chained.IdleTimeout) + resetTimer := func() { + if !t.Stop() { + <-t.C + } + next := time.Duration(chained.IdleTimeout) + t.Reset(next) + } + + for { + select { + case <-cb.received: + resetTimer() + case <-cb.wrote: + resetTimer() + case <-t.C: + log.Debugf("Timing out idle connection to %v", cb.target) + cb.upstream.Close() // we don't close downstream because that'll happen automatically once upstream finishes closing + return + } + } +} + +// directUDPHandler implements UDPConnHandler from go-tun2socks by sending UDP traffic directly to +// the origin. It is loosely based on https://github.com/eycorsican/go-tun2socks/blob/master/proxy/socks/udp.go +type directUDPHandler struct { + sync.RWMutex + + client *iosClient + dialer UDPDialer + grabber dnsgrab.Server + capturedDNSHost string + + downstreamWriteWorker *worker + upstreams map[core.UDPConn]UDPConn + dialingConns int64 +} + +func newDirectUDPHandler(client *iosClient, dialer UDPDialer, grabber dnsgrab.Server, capturedDNSHost string) *directUDPHandler { + result := &directUDPHandler{ + client: client, + dialer: dialer, + capturedDNSHost: capturedDNSHost, + grabber: grabber, + downstreamWriteWorker: newWorker(100), + upstreams: make(map[core.UDPConn]UDPConn), + } + go result.trackStats() + return result +} + +func (h *directUDPHandler) Connect(downstream core.UDPConn, target *net.UDPAddr) error { + if target.IP.String() == h.capturedDNSHost && target.Port == 53 { + // Captured dns, handle internally with dnsgrab + return nil + } + + // Since UDP traffic is sent directly, do a reverse lookup of the IP and then resolve the UDP address + host, found := h.grabber.ReverseLookup(target.IP) + if !found { + return log.Errorf("Unknown IP %v, not connecting", target.IP) + } + if found { + ip, err := net.ResolveIPAddr("ip", host) + if err != nil { + return log.Errorf("Unable to resolve IP address for %v, not connecting: %v", host, err) + } + target.IP = ip.IP + } + + // Dial + atomic.AddInt64(&h.dialingConns, 1) + defer atomic.AddInt64(&h.dialingConns, -1) + + // note - the below convoluted flow is necessary because of limitations in what kind + // of APIs can be bound to Swift using gomobile. + upstream := h.dialer.Dial(target.IP.String(), target.Port) + + cb := &UDPCallbacks{ + h: h, + // MEMORY_OPTIMIZATION - use a threadLimitingUDPConn to limit the number of goroutines that are writing to LWIP + downstream: newThreadLimitingUDPConn(downstream, h.downstreamWriteWorker), + upstream: upstream, + target: target, + dialSucceeded: make(chan interface{}), + dialFailed: make(chan interface{}), + received: make(chan interface{}, 10), + wrote: make(chan interface{}, 10), + } + + upstream.RegisterCallbacks(cb) + + select { + case <-cb.dialFailed: + return log.Errorf("Failed to dial %v", target) + case <-cb.dialSucceeded: + h.Lock() + h.upstreams[downstream] = upstream + h.Unlock() + + // Request to receive first datagram + upstream.ReceiveDatagram() + case <-time.After(dialTimeout): + upstream.Close() + return log.Errorf("Timed out dialing %v", target) + } + + go cb.idleTiming() + + return nil +} + +func (h *directUDPHandler) ReceiveTo(downstream core.UDPConn, data []byte, addr *net.UDPAddr) error { + h.RLock() + upstream := h.upstreams[downstream] + h.RUnlock() + + if upstream == nil { + // if there's no upstream, that means this is a DNS query + return h.receiveDNS(downstream, data, addr) + } + + upstream.WriteDatagram(data) + return nil +} + +func (h *directUDPHandler) receiveDNS(downstream core.UDPConn, data []byte, addr *net.UDPAddr) error { + response, numAnswers, err := h.grabber.ProcessQuery(data) + if err != nil { + return log.Errorf("Unable to process dns query: %v", err) + } + + if numAnswers == 0 { + // nothing to write + return nil + } + + // MEMORY_OPTIMIZATION - use a threadLimitingUDPConn to limit the number of goroutines that are writing to LWIP + _, writeErr := newThreadLimitingUDPConn(downstream, h.downstreamWriteWorker).WriteFrom(response, addr) + return writeErr +} + +func (h *directUDPHandler) trackStats() { + for { + h.RLock() + activeConns := len(h.upstreams) + h.RUnlock() + + statsLog.Debugf("UDP Conns Active: %d Dialing: %d", activeConns, atomic.LoadInt64(&h.dialingConns)) + time.Sleep(1 * time.Second) + } +} diff --git a/internalsdk/issue.go b/internalsdk/issue.go index f09e844a7..c5ed11561 100644 --- a/internalsdk/issue.go +++ b/internalsdk/issue.go @@ -35,7 +35,7 @@ func SendIssueReport( return err } return issue.SendReport( - newUserConfig(&panickingSessionImpl{session}), + NewUserConfig(&panickingSessionImpl{session}), issueTypeInt, description, subscriptionLevel, diff --git a/internalsdk/session_model.go b/internalsdk/session_model.go index ab6fd5bf1..822fccf39 100644 --- a/internalsdk/session_model.go +++ b/internalsdk/session_model.go @@ -3,14 +3,13 @@ package internalsdk import ( "context" "fmt" - "net/http" - "path/filepath" "strconv" "time" "github.com/getlantern/errors" - "github.com/getlantern/flashlight/v7/logging" "github.com/getlantern/flashlight/v7/proxied" + + //"github.com/getlantern/flashlight/v7/proxied" "github.com/getlantern/lantern-client/internalsdk/common" "github.com/getlantern/lantern-client/internalsdk/pro" "github.com/getlantern/lantern-client/internalsdk/protos" @@ -86,17 +85,15 @@ func NewSessionModel(mdb minisql.DB, opts *SessionModelOpts) (*SessionModel, err if err != nil { return nil, err } + dialTimeout := 30 * time.Second if opts.Platform == "ios" { + dialTimeout = 20 * time.Second base.db.RegisterType(1000, &protos.ServerInfo{}) base.db.RegisterType(2000, &protos.Devices{}) } m := &SessionModel{baseModel: base} - m.proClient = pro.NewClient(fmt.Sprintf("https://%s", common.ProAPIHost), &pro.Opts{ - HttpClient: &http.Client{ - Transport: proxied.ParallelForIdempotent(), - Timeout: 30 * time.Second, - }, + HttpClient: proxied.DirectThenFrontedClient(dialTimeout), UserConfig: func() common.UserConfig { deviceID, _ := m.GetDeviceID() userID, _ := m.GetUserID() @@ -114,7 +111,8 @@ func NewSessionModel(mdb minisql.DB, opts *SessionModelOpts) (*SessionModel, err }) m.baseModel.doInvokeMethod = m.doInvokeMethod - return m, m.initSessionModel(context.Background(), opts) + go m.initSessionModel(context.Background(), opts) + return m, nil } func (m *SessionModel) doInvokeMethod(method string, arguments Arguments) (interface{}, error) { @@ -184,7 +182,7 @@ func (m *SessionModel) doInvokeMethod(method string, arguments Arguments) (inter case "createUser": err := m.userCreate(context.Background(), arguments.Scalar().String()) if err != nil { - return nil, err + log.Error(err) } return true, nil case "hasAllNetworkPermssion": @@ -196,23 +194,23 @@ func (m *SessionModel) doInvokeMethod(method string, arguments Arguments) (inter } checkAdsEnabled(m) return true, nil + case "updateStats": + city := arguments.Get("city").String() + country := arguments.Get("country").String() + serverCountryCode := arguments.Get("serverCountryCode").String() + httpsUpgrades := arguments.Get("httpsUpgrades").Int() + adsBlocked := arguments.Get("adsBlocked").Int() + hasSucceedingProxy := arguments.Get("hasSucceedingProxy").Bool() + err := m.UpdateStats(city, country, serverCountryCode, httpsUpgrades, adsBlocked, hasSucceedingProxy) + if err != nil { + return nil, err + } + return true, nil default: return m.methodNotImplemented(method) } } -// Internal functions that manage method -func (m *SessionModel) StartService(configDir string, - locale string, - settings Settings) { - logging.EnableFileLogging(common.DefaultAppName, filepath.Join(configDir, "logs")) - session := &panickingSessionImpl{m} - startOnce.Do(func() { - go run(configDir, locale, settings, session) - }) - -} - // InvokeMethod handles method invocations on the SessionModel. func (m *SessionModel) initSessionModel(ctx context.Context, opts *SessionModelOpts) error { // Check if email if empty @@ -275,19 +273,20 @@ func (m *SessionModel) initSessionModel(ctx context.Context, opts *SessionModelO if userId == 0 { local, err := m.Locale() if err != nil { - return err - } - // Create user - err = m.userCreate(ctx, local) - if err != nil { - return err + log.Error(err) + } else { + // Create user + err = m.userCreate(ctx, local) + if err != nil { + log.Error(err) + } } } // Get all user details err = m.userDetail(ctx) if err != nil { - return err + log.Error(err) } return checkAdsEnabled(m) } diff --git a/internalsdk/stats_tracker.go b/internalsdk/stats_tracker.go index ed9faaddb..7b3da7807 100644 --- a/internalsdk/stats_tracker.go +++ b/internalsdk/stats_tracker.go @@ -6,10 +6,10 @@ import ( type statsTracker struct { stats.Tracker - session panickingSession + session PanickingSession } -func NewStatsTracker(session panickingSession) *statsTracker { +func NewStatsTracker(session PanickingSession) *statsTracker { s := &statsTracker{ Tracker: stats.NewTracker(), session: session, diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml index 056efef3e..ec05fd9ad 100644 --- a/ios/.swiftlint.yml +++ b/ios/.swiftlint.yml @@ -1,9 +1,15 @@ disabled_rules: - trailing_whitespace - identifier_name +- line_length opt_in_rules: - empty_count - empty_string +- vertical_whitespace_closing_braces +- redundant_optional_initialization +- explicit_self +- vertical_parameter_alignment +- weak_delegate included: # paths to include during linting. `--path` is ignored if present. - Source - Runner diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e2abbaaf6..b70495750 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,41 +4,38 @@ PODS: - Flutter - audioplayers_darwin (0.0.1): - Flutter - - clever_ads_solutions (0.0.1): - - CleverAdsSolutions-Base (~> 3.5.2) - - Flutter - CleverAdsSolutions-Base (3.5.6) - device_info_plus (0.0.1): - Flutter - - DKImagePickerController/Core (4.3.4): + - DKImagePickerController/Core (4.3.9): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.4) - - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): - DKImagePickerController/Core - DKPhotoGallery - - DKImagePickerController/Resource (4.3.4) - - DKPhotoGallery (0.0.17): - - DKPhotoGallery/Core (= 0.0.17) - - DKPhotoGallery/Model (= 0.0.17) - - DKPhotoGallery/Preview (= 0.0.17) - - DKPhotoGallery/Resource (= 0.0.17) + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) - SDWebImage - SwiftyGif - - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Core (0.0.19): - DKPhotoGallery/Model - DKPhotoGallery/Preview - SDWebImage - SwiftyGif - - DKPhotoGallery/Model (0.0.17): + - DKPhotoGallery/Model (0.0.19): - SDWebImage - SwiftyGif - - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Preview (0.0.19): - DKPhotoGallery/Model - DKPhotoGallery/Resource - SDWebImage - SwiftyGif - - DKPhotoGallery/Resource (0.0.17): + - DKPhotoGallery/Resource (0.0.19): - SDWebImage - SwiftyGif - emoji_picker_flutter (0.0.1): @@ -68,59 +65,13 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast - - Google-Mobile-Ads-SDK (10.11.0): - - GoogleAppMeasurement (< 11.0, >= 7.0) + - Google-Mobile-Ads-SDK (11.2.0): - GoogleUserMessagingPlatform (>= 1.1) - google_mobile_ads (1.0.0): - Flutter - - Google-Mobile-Ads-SDK (~> 10.11.0) + - Google-Mobile-Ads-SDK (~> 11.2.0) - webview_flutter_wkwebview - - GoogleAppMeasurement (10.23.0): - - GoogleAppMeasurement/AdIdSupport (= 10.23.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - - GoogleUtilities/MethodSwizzler (~> 7.11) - - GoogleUtilities/Network (~> 7.11) - - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.23.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.23.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - - GoogleUtilities/MethodSwizzler (~> 7.11) - - GoogleUtilities/Network (~> 7.11) - - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.23.0): - - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - - GoogleUtilities/MethodSwizzler (~> 7.11) - - GoogleUtilities/Network (~> 7.11) - - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleUserMessagingPlatform (2.3.0) - - GoogleUtilities/AppDelegateSwizzler (7.13.0): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.0): - - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.0): - - GoogleUtilities/Environment - - GoogleUtilities/Privacy - - GoogleUtilities/MethodSwizzler (7.13.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.0): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Privacy - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.0)": - - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.0) - - GoogleUtilities/Reachability (7.13.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy + - GoogleUserMessagingPlatform (2.4.0) - integration_test (0.0.1): - Flutter - libwebp (1.3.2): @@ -136,11 +87,6 @@ PODS: - libwebp/webp (1.3.2): - libwebp/sharpyuv - MTBBarcodeScanner (5.0.11) - - nanopb (2.30910.0): - - nanopb/decode (= 2.30910.0) - - nanopb/encode (= 2.30910.0) - - nanopb/decode (2.30910.0) - - nanopb/encode (2.30910.0) - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter @@ -149,13 +95,12 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - PromisesObjC (2.4.0) - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner - - SDWebImage (5.19.1): - - SDWebImage/Core (= 5.19.1) - - SDWebImage/Core (5.19.1) + - SDWebImage (5.19.2): + - SDWebImage/Core (= 5.19.2) + - SDWebImage/Core (5.19.2) - Sentry/HybridSDK (8.21.0): - SentryPrivate (= 8.21.0) - sentry_flutter (0.0.1): @@ -171,8 +116,8 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS - - SwiftyGif (5.4.4) - - Toast (4.1.0) + - SwiftyGif (5.4.5) + - Toast (4.1.1) - Toast-Swift (5.0.1) - url_launcher_ios (0.0.1): - Flutter @@ -190,7 +135,6 @@ PODS: DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) - - clever_ads_solutions (from `.symlinks/plugins/clever_ads_solutions/ios`) - CleverAdsSolutions-Base (~> 3.5.1) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) @@ -228,14 +172,10 @@ SPEC REPOS: - DKImagePickerController - DKPhotoGallery - Google-Mobile-Ads-SDK - - GoogleAppMeasurement - GoogleUserMessagingPlatform - - GoogleUtilities - libwebp - MTBBarcodeScanner - - nanopb - OrderedSet - - PromisesObjC - SDWebImage - Sentry - SentryPrivate @@ -248,8 +188,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/app_links/ios" audioplayers_darwin: :path: ".symlinks/plugins/audioplayers_darwin/ios" - clever_ads_solutions: - :path: ".symlinks/plugins/clever_ads_solutions/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" emoji_picker_flutter: @@ -307,11 +245,10 @@ SPEC CHECKSUMS: Alamofire: 814429acc853c6c54ff123fc3d2ef66803823ce0 app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 - clever_ads_solutions: 4329310a15fd616aee3de8f208018fa1621e7c66 CleverAdsSolutions-Base: b2b98815a732a72d75dadfde430c5f24eafafdd5 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac - DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 emoji_picker_flutter: fe2e6151c5b548e975d546e6eeb567daf0962a58 file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 @@ -322,30 +259,26 @@ SPEC CHECKSUMS: flutter_pdfview: 25f53dd6097661e6395b17de506e6060585946bd flutter_uploader: 2b07c4d41cf1218f25e6d5b8bcfa2d913838e27c fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 - Google-Mobile-Ads-SDK: 58b4fda3f9758fc1ed210aa5cf7777b5eb55d47e - google_mobile_ads: 511febb4768edc860ee455a9e201ff52de385908 - GoogleAppMeasurement: 453eb0de99fcf2bdec9403e9ac5d7871fdba3e3f - GoogleUserMessagingPlatform: b1ad7bb1ee3ce64749d4fec24f667b36e45c396c - GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 + Google-Mobile-Ads-SDK: 5a6d005a6cb5b5e8f4c7b69ca05cdea79c181139 + google_mobile_ads: 5698dd1cd5a3cfc6688abec7db5428f237d6b1ac + GoogleUserMessagingPlatform: f131fa7978d2ba88d7426702b057c2cc318e6595 integration_test: 13825b8a9334a850581300559b8839134b124670 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb - nanopb: 438bc412db1928dac798aa6fd75726007be04262 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e - SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb + SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a Sentry: ebc12276bd17613a114ab359074096b6b3725203 sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - Toast: ec33c32b8688982cecc6348adeae667c1b9938da + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e Toast-Swift: 9b6a70f28b3bf0b96c40d46c0c4b9d6639846711 url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 @@ -355,4 +288,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f41df053e13795ca3bec060038c249ef06910792 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 206da089f..1f90cf6e9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,12 +14,12 @@ 0308A1312AAEF3B00086157A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0308A12F2AAEF3AA0086157A /* Event.swift */; }; 0308A1342AAEF5130086157A /* VPNBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A1AB152AA88BF200FB41B2 /* VPNBase.swift */; }; 0308A1392AAEF8DE0086157A /* FlashlightManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321C3F82AA9D8DF00D462C1 /* FlashlightManager.swift */; }; - 0308A13D2AAF39F80086157A /* FlashlightManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0308A13C2AAF39F80086157A /* FlashlightManagerExtension.swift */; }; 0308A13E2AAF43A10086157A /* UserNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0308A12B2AAEEE2C0086157A /* UserNotificationsManager.swift */; }; 0321C3F72AA9D84700D462C1 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321C3F62AA9D84700D462C1 /* Process.swift */; }; 0321C3F92AA9D8DF00D462C1 /* FlashlightManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321C3F82AA9D8DF00D462C1 /* FlashlightManager.swift */; }; 0324CC302AE279CF00490B46 /* SentryUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0324CC2F2AE279CF00490B46 /* SentryUtils.swift */; }; 032A43582AA6F8AA00EF442B /* DnsDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032A43572AA6F8AA00EF442B /* DnsDetector.swift */; }; + 0332DD612BFF809C0007240D /* StatsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0332DD602BFF809C0007240D /* StatsTracker.swift */; }; 0333203B2AC698FA00333DA9 /* MockVPNManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0333203A2AC698FA00333DA9 /* MockVPNManager.swift */; }; 03567DBB2AA9F15100A233EA /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A1AB432AA8946400FB41B2 /* Constants.swift */; }; 03567DBC2AA9F18D00A233EA /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321C3F62AA9D84700D462C1 /* Process.swift */; }; @@ -44,6 +44,7 @@ 03FAF1B42AA1E9F40063580C /* VpnModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FAF1B32AA1E9F40063580C /* VpnModel.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5FA8BFF12BFB34F80097D1B9 /* FlashlightManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA8BFF02BFB34F80097D1B9 /* FlashlightManagerExtension.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -94,12 +95,12 @@ 0308A12B2AAEEE2C0086157A /* UserNotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationsManager.swift; sourceTree = ""; }; 0308A12D2AAEEF410086157A /* Events.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = ""; }; 0308A12F2AAEF3AA0086157A /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; - 0308A13C2AAF39F80086157A /* FlashlightManagerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlashlightManagerExtension.swift; sourceTree = ""; }; 0315F55C2AC1C1A500D49F7B /* DatabaseFramework.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DatabaseFramework.framework; path = "../../../../../Library/Developer/Xcode/DerivedData/DatabaseFramework-csnjouuwjwleiddbptbupimqigld/Build/Products/Debug-iphoneos/DatabaseFramework.framework"; sourceTree = ""; }; 0321C3F62AA9D84700D462C1 /* Process.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = ""; }; 0321C3F82AA9D8DF00D462C1 /* FlashlightManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlashlightManager.swift; sourceTree = ""; }; 0324CC2F2AE279CF00490B46 /* SentryUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUtils.swift; sourceTree = ""; }; 032A43572AA6F8AA00EF442B /* DnsDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DnsDetector.swift; sourceTree = ""; }; + 0332DD602BFF809C0007240D /* StatsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTracker.swift; sourceTree = ""; }; 0333203A2AC698FA00333DA9 /* MockVPNManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVPNManager.swift; sourceTree = ""; }; 03567DBF2AA9F47200A233EA /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; 03567DC22AA9F77000A233EA /* NEProviderStopReason.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NEProviderStopReason.swift; sourceTree = ""; }; @@ -137,6 +138,7 @@ 40CA215A5360960236859E29 /* Pods-Runner.debug-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-prod.xcconfig"; sourceTree = ""; }; 41DDE2A6029CFDB93A269403 /* Pods-LanternTests.debug-runner-appium-test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LanternTests.debug-runner-appium-test.xcconfig"; path = "Target Support Files/Pods-LanternTests/Pods-LanternTests.debug-runner-appium-test.xcconfig"; sourceTree = ""; }; 4C199F107C36A050FA0BD82A /* Pods-Runner.debug-appium-test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-appium-test.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-appium-test.xcconfig"; sourceTree = ""; }; + 5FA8BFF02BFB34F80097D1B9 /* FlashlightManagerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlashlightManagerExtension.swift; sourceTree = ""; }; 607A0530FA4F0B857B847CC5 /* Pods-Runner.profile-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-prod.xcconfig"; sourceTree = ""; }; 639C2FD1FB8C2B8BA0BC8FA2 /* Pods-LanternTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LanternTests.profile.xcconfig"; path = "Target Support Files/Pods-LanternTests/Pods-LanternTests.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; @@ -231,9 +233,9 @@ 03A1AB122AA88BB400FB41B2 /* Vpn */ = { isa = PBXGroup; children = ( - 0308A13C2AAF39F80086157A /* FlashlightManagerExtension.swift */, 0308A1272AAEED7E0086157A /* VpnHelper.swift */, 0321C3F82AA9D8DF00D462C1 /* FlashlightManager.swift */, + 5FA8BFF02BFB34F80097D1B9 /* FlashlightManagerExtension.swift */, 03A1AB152AA88BF200FB41B2 /* VPNBase.swift */, 03A1AB132AA88BDA00FB41B2 /* VPNManager.swift */, 0333203A2AC698FA00333DA9 /* MockVPNManager.swift */, @@ -249,6 +251,7 @@ 03A1AB342AA890AB00FB41B2 /* PacketTunnelProvider.swift */, 03A1AB362AA890AB00FB41B2 /* Info.plist */, 03A1AB372AA890AB00FB41B2 /* Tunnel.entitlements */, + 0332DD602BFF809C0007240D /* StatsTracker.swift */, ); path = Tunnel; sourceTree = ""; @@ -464,7 +467,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 03A1AB3F2AA890AB00FB41B2 /* Embed Foundation Extensions */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 3F72706DAEB45E756D135F84 /* [CP] Embed Pods Frameworks */, 8CFC94838D10599DD335E9EC /* [CP] Copy Pods Resources */, 03ACAAB62AC5B7ED00D74DD4 /* ShellScript */, ); @@ -506,7 +508,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 03A1AB2F2AA890AB00FB41B2 = { @@ -668,6 +670,7 @@ 03567DBE2AA9F31200A233EA /* Logger.swift in Sources */, 0308A13E2AAF43A10086157A /* UserNotificationsManager.swift in Sources */, 0308A1312AAEF3B00086157A /* Event.swift in Sources */, + 0332DD612BFF809C0007240D /* StatsTracker.swift in Sources */, 03A1AB352AA890AB00FB41B2 /* PacketTunnelProvider.swift in Sources */, 03567DBB2AA9F15100A233EA /* Constants.swift in Sources */, 03567DC12AA9F47200A233EA /* FileUtils.swift in Sources */, @@ -700,11 +703,11 @@ 0308A1302AAEF3AA0086157A /* Event.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 03A1AB142AA88BDA00FB41B2 /* VPNManager.swift in Sources */, - 0308A13D2AAF39F80086157A /* FlashlightManagerExtension.swift in Sources */, 03567DC02AA9F47200A233EA /* FileUtils.swift in Sources */, 03802FA32A98C9B000DACDFC /* SubscriberRequest.swift in Sources */, 03A1AB422AA8930A00FB41B2 /* Result.swift in Sources */, 0324CC302AE279CF00490B46 /* SentryUtils.swift in Sources */, + 5FA8BFF12BFB34F80097D1B9 /* FlashlightManagerExtension.swift in Sources */, 0308A1282AAEED7E0086157A /* VpnHelper.swift in Sources */, 03F00B5F2AB07C8000E991E2 /* LoadingIndicatorManager.swift in Sources */, EC6155E92ACC8FF70015527E /* NavigationModel.swift in Sources */, diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme index 6abe0af3c..fdd117926 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme @@ -49,6 +49,13 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..cb4b75775 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", + "version" : "0.14.1" + } + } + ], + "version" : 2 +} diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index a544c5741..0bfefe967 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -4,20 +4,16 @@ import SQLite import Toast_Swift import UIKit -//know Issue -//  CFPrefsPlistSource<0x28281e580> (Domain: group.getlantern.lantern, User: kCFPreferencesAnyUser, ByHost: Yes, Container: (null), Contents Need Refresh: Yes): Using kCFPreferencesAnyUser with a container is only allowed for System Containers, - @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { // Flutter Properties var flutterViewController: FlutterViewController! var flutterbinaryMessenger: FlutterBinaryMessenger! // Model Properties - var sessionModel: SessionModel! - var lanternModel: LanternModel! - // var navigationModel: NavigationModel! - var vpnModel: VpnModel! - var messagingModel: MessagingModel! + private var sessionModel: SessionModel! + private var lanternModel: LanternModel! + private var vpnModel: VpnModel! + private var messagingModel: MessagingModel! // IOS var loadingManager: LoadingIndicatorManager? @@ -25,7 +21,6 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - // SentryUtils.startSentry(); initializeFlutterComponents() do { try setupAppComponents() @@ -45,20 +40,9 @@ import UIKit // Intlize this GO model and callback private func setupAppComponents() throws { - DispatchQueue.global(qos: .userInitiated).async { - do { - try self.setupModels() - DispatchQueue.main.async { - self.startUpSequency() - self.setupLoadingBar() - } - } catch { - DispatchQueue.main.async { - logger.error("Unexpected error setting up models: \(error)") - } - } - } - + try self.setupModels() + self.startUpSequency() + self.setupLoadingBar() } // Init all the models diff --git a/ios/Runner/Lantern/Core/Vpn/VPNManager.swift b/ios/Runner/Lantern/Core/Vpn/VPNManager.swift index f806af644..7f4efdc6c 100644 --- a/ios/Runner/Lantern/Core/Vpn/VPNManager.swift +++ b/ios/Runner/Lantern/Core/Vpn/VPNManager.swift @@ -3,6 +3,7 @@ // Lantern // +import Internalsdk import Network import NetworkExtension @@ -208,6 +209,18 @@ class VPNManager: VPNBase { let config = NETunnelProviderProtocol() config.providerBundleIdentifier = Constants.netExBundleId config.serverAddress = "0.0.0.0" // needs to be set but purely 8.8.8.8 + + // var conf = [String: AnyObject]() + // + // let httpPort = IosHTTPProxyPort() + // + // logger.log("HTTP proxy port is \(httpPort)") + // + // conf["proxyHost"] = "127.0.0.1" as AnyObject + // conf["proxyPort"] = String(httpPort) as AnyObject + + // config.providerConfiguration = conf + provider.protocolConfiguration = config provider.isEnabled = true // calling start when disabled crashes // Set rules for onDemand... diff --git a/ios/Runner/Lantern/Core/Vpn/VpnHelper.swift b/ios/Runner/Lantern/Core/Vpn/VpnHelper.swift index e5c9c600b..e10f71a22 100644 --- a/ios/Runner/Lantern/Core/Vpn/VpnHelper.swift +++ b/ios/Runner/Lantern/Core/Vpn/VpnHelper.swift @@ -61,7 +61,6 @@ class VpnHelper: NSObject { } } - static let hasFetchedConfigDefaultsKey = "Lantern.hasConfig" // Apple let fileManager: FileManager let userDefaults: UserDefaults @@ -70,11 +69,12 @@ class VpnHelper: NSObject { let constants: Constants let flashlightManager: FlashlightManager let vpnManager: VPNBase - var configFetchTimer: Timer! + // var configFetchTimer: Timer! var hasConfiguredThisSession = false - var hasFetchedConfigOnce: Bool { - return (userDefaults.value(forKey: VpnHelper.hasFetchedConfigDefaultsKey) as? Bool) ?? false - } + var configFetchInProcess: Bool = false + + private var configFetchTimer: DispatchSourceTimer? + private let fetchQueue = DispatchQueue(label: "org.getlantern.configfetch", qos: .background) init( constants: Constants, @@ -94,7 +94,7 @@ class VpnHelper: NSObject { configuring = true _state = .idle(nil) super.init() - if self.hasFetchedConfigOnce { + if self.hasConfiguredThisSession { self.configuring = false } performAppSetUp() @@ -126,7 +126,7 @@ class VpnHelper: NSObject { // create process-specific directory @ ~/app do { - try fileManager.ensureDirectoryExists(at: Constants.lanternDirectory) + try fileManager.ensureDirectoryExists(at: Constants.lanternDirectory) } catch { logger.error("Failed to create directory @ \(Constants.lanternDirectory.path)") } @@ -166,7 +166,7 @@ class VpnHelper: NSObject { onSuccess: (() -> Void)? = nil ) { guard state.isIdle else { return } - if !hasFetchedConfigOnce { + if !hasConfiguredThisSession { initiateConfigFetching(onError: onError, onSuccess: onSuccess) } else { initiateVPNStart(onError: onError, onSuccess: onSuccess) @@ -237,25 +237,112 @@ class VpnHelper: NSObject { } func fetchConfigIfNecessary() { - logger.debug("Checking if config fetch is needed") - guard !self.hasConfiguredThisSession else { return } - logger.debug("Will fetch config") - self.hasConfiguredThisSession = true - let hasFetchedConfigOnce = self.hasFetchedConfigOnce + guard configFetchInProcess == false else { + logger.debug("Config fetch is already in progress.") + return + } + logger.debug("Checking if config fetch is needed for state: \(state)") + if !self.hasConfiguredThisSession { + logger.debug("Config fetch is needed for first time") + self.fetchAndSetUpTimer() + return + } + + guard state == .connected else { + logger.debug("Config fetch not needed: state is not connected.") + return + } + + logger.debug("State is connected, will fetch config") + self.fetchAndSetUpTimer() + } + + private func fetchAndSetUpTimer() { + configFetchInProcess = true + logger.debug("Starting config fetch...") fetchConfig { [weak self] result in - self?.setUpConfigFetchTimer() DispatchQueue.main.async { - self?.configuring = false - if let state = self?.state { - guard state.isIdle else { return } + guard let strongSelf = self else { return } + switch result { + case .success: + logger.debug("Config fetch succeeded.") + strongSelf.configuring = false + strongSelf.hasConfiguredThisSession = true + strongSelf.configFetchInProcess = false + case .failure(let error): + logger.error("Config fetch failed: \(error.localizedDescription)") + if !strongSelf.hasConfiguredThisSession { + strongSelf.state = .configuring + // if the config not fetched we need to try again + strongSelf.fetchAndSetUpTimer() + return + } } - if !result.isSuccess && !hasFetchedConfigOnce { - self?.state = .idle(.unableToFetchConfig) + // Start the timer to fetch config periodically if connected + strongSelf.setUpConfigFetchTimer() + } + } + } + + // this use DispatchSource more efficient method + func setUpConfigFetchTimer() { + // Cancel any existing timer + configFetchTimer?.cancel() + configFetchTimer = nil + + // Create a new DispatchSourceTimer + configFetchTimer = DispatchSource.makeTimerSource(queue: fetchQueue) + + let time: Double = 60 + configFetchTimer?.schedule(deadline: .now() + time, repeating: time) + + configFetchTimer?.setEventHandler { [weak self] in + // Only auto-fetch new config when VPN is on + guard self?.state == .connected else { + logger.debug("Skipping config fetch because state is not connected.") + return + } + logger.debug("Config Fetch timer fired after \(time) seconds, fetching...") + self?.fetchConfig { result in + switch result { + case .success: + logger.debug("Auto-config fetch success") + case .failure(let error): + logger.error("Auto-config fetch failed: \(error.localizedDescription)") } } } + + // Start the timer + configFetchTimer?.resume() } + // private func startConfigFetchTimer() { + // DispatchQueue.main.async { [weak self] in + // guard let strongSelf = self else { return } + // let time: TimeInterval = 60 + // strongSelf.configFetchTimer?.invalidate() // Invalidate any existing timer + // logger.debug("Setting up config fetch timer with interval: \(time) seconds") + // strongSelf.configFetchTimer = Timer.scheduledTimer(withTimeInterval: time, repeats: true) { + // [weak self] _ in + // guard let strongSelf = self else { return } + // guard strongSelf.state == .connected else { + // logger.debug("Skipping config fetch because state is not connected.") + // return + // } + // logger.debug("Config fetch timer fired, fetching config...") + // strongSelf.fetchConfig { result in + // switch result { + // case .success: + // logger.debug("Auto-config fetch succeeded.") + // case .failure(let error): + // logger.error("Auto-config fetch failed: \(error.localizedDescription)") + // } + // } + // } + // } + // } + func fetchConfig( refreshProxies: Bool = true, _ completion: @escaping (Result) -> Void ) { @@ -268,7 +355,6 @@ class VpnHelper: NSObject { if vpnNeedsReconfiguring { self?.messageNetExToUpdateExcludedIPs() } - self?.userDefaults.set(refreshProxies, forKey: VpnHelper.hasFetchedConfigDefaultsKey) logger.debug("Successfully fetched new config with \(result)") completion(.success(())) case .failure(let error): @@ -279,29 +365,6 @@ class VpnHelper: NSObject { } } - func setUpConfigFetchTimer() { - // set up timer on Main queue's runloop - // FlashlightManager will automatically use its designated goQueue when fetching - DispatchQueue.main.async { [weak self] in - let time: Double = 60 - self?.configFetchTimer = Timer.scheduledTimer( - withTimeInterval: time, repeats: true, - block: { [weak self] _ in - // Only auto-fetch new config when VPN is on - guard self?.state == .connected else { return } - logger.debug("Config Fetch timer fired after \(time), fetching...") - self?.fetchConfig { result in - switch result { - case .success: - logger.debug("Auto-config fetch success") - case .failure(let error): - logger.error("Auto-config fetch failed: \(error.localizedDescription)") - } - } - }) - } - } - private func messageNetExToUpdateExcludedIPs() { logger.debug("Notifying network extension of updated config") do { diff --git a/ios/Runner/Lantern/Models/BaseModel.swift b/ios/Runner/Lantern/Models/BaseModel.swift index b9d733160..410acc48f 100644 --- a/ios/Runner/Lantern/Models/BaseModel.swift +++ b/ios/Runner/Lantern/Models/BaseModel.swift @@ -22,6 +22,7 @@ open class BaseModel: NSObject, FlutterStreamHandle var activeSubscribers: Set = [] private let mainHandler = DispatchQueue.main private let asyncHandler = DispatchQueue(label: "BaseModel-AsyncHandler") + private let invokeBackgroundQueue = DispatchQueue.global(qos: .background) init(_ flutterBinary: FlutterBinaryMessenger, _ model: M) throws { self.model = model @@ -186,8 +187,23 @@ open class BaseModel: NSObject, FlutterStreamHandle return FlutterError(code: code, message: message, details: details) } - internal func invoke(_ name: String, _ arguments: Any = "") throws -> MinisqlValue? { - return try model.invokeMethod(name, arguments: try Arguments(arguments)) + internal func invoke( + _ name: String, arguments: Any = "", + completion: @escaping (MinisqlValue?, Error?) -> Void + ) { + // Dispatch the invoke call asynchronously on the custom queue + invokeBackgroundQueue.async { + do { + let result = try self.model.invokeMethod(name, arguments: try Arguments(arguments)) + DispatchQueue.main.async { + completion(result, nil) + } + } catch { + DispatchQueue.main.async { + completion(nil, error) + } + } + } } } diff --git a/ios/Runner/Lantern/Models/SessionModel.swift b/ios/Runner/Lantern/Models/SessionModel.swift index 1cac33004..6bbff6b8c 100644 --- a/ios/Runner/Lantern/Models/SessionModel.swift +++ b/ios/Runner/Lantern/Models/SessionModel.swift @@ -15,12 +15,14 @@ class SessionModel: BaseModel { lazy var notificationsManager: UserNotificationsManager = { return UserNotificationsManager() }() + let emptyCompletion: (MinisqlValue?, Error?) -> Void = { _, _ in } + private let sessionAsyncHandler = DispatchQueue.global(qos: .background) init(flutterBinary: FlutterBinaryMessenger) throws { let opts = InternalsdkSessionModelOpts() let device = UIDevice.current let deviceId = device.identifierForVendor!.uuidString - let model = device.model + let deviceModel = device.model let systemName = device.systemName let systemVersion = device.systemVersion opts.deviceID = deviceId @@ -30,11 +32,10 @@ class SessionModel: BaseModel { opts.playVersion = (isRunningFromAppStore() || isRunningInTestFlightEnvironment()) opts.timeZone = TimeZone.current.identifier opts.device = systemName // IOS does not provide Device name directly - opts.model = systemName + opts.model = deviceModel opts.osVersion = systemVersion opts.paymentTestMode = AppEnvironment.current == AppEnvironment.appiumTest opts.platform = "ios" - var error: NSError? guard let model = InternalsdkNewSessionModel( @@ -43,94 +44,101 @@ class SessionModel: BaseModel { throw error! } try super.init(flutterBinary, model) -// DispatchQueue.global(qos: .userInitiated).async { -// self.startService() -// } - } - - func startService() { - // let configDir = configDirFor(suffix: "service") - (model as! InternalsdkSessionModel).startService( - Constants.lanternDirectory.path, locale: "en", settings: Settings()) - logger.error("Service Started successfully") + observeStatsUpdates() + getUserId() + getProToken() } func hasAllPermssion() { do { - let result = try invoke("hasAllNetworkPermssion") + let result = try invoke("hasAllNetworkPermssion", completion: emptyCompletion) logger.log("Sucessfully given all permssion") } catch { logger.log("Error while setting hasAllPermssion") SentryUtils.caputure(error: error as NSError) } } - - func getBandwidth() { - // TODO: we should do this reactively by subscribing - do { - let result = try invoke("getBandwidth") - let newValue = ValueUtil.convertFromMinisqlValue(from: result!) - let limit = newValue as! Int - if limit == 100 { - // if user has reached limit show the notificaiton - notificationsManager.scheduleDataCapLocalNotification(withDataLimit: limit) + private func getUserId() { + sessionAsyncHandler.async { + do { + var userID: Int64 = 0 + try self.model.getUserID(&userID) + DispatchQueue.main.async { + if userID != 0 { + Constants.appGroupDefaults.set(userID, forKey: Constants.userID) + logger.log("Successfully got user id \(userID)") + } else { + logger.log("Failed to get user id") + } + } + } catch { + DispatchQueue.main.async { + SentryUtils.caputure(error: error as NSError) + } } - logger.log("Sucessfully getbandwidth \(newValue)") - } catch { - logger.log("Error while getting bandwidth") - SentryUtils.caputure(error: error as NSError) } } - private func setDNS() { - // TODO: why are we setting timezone in setDNS()? - // let timeZoneId = TimeZone.current.identifier - // let miniSqlValue = ValueUtil.convertToMinisqlValue(DnsDetector.DEFAULT_DNS_SERVER) - // if miniSqlValue != nil { - // do { - // let result = try invokeMethodOnGo("setTimeZone", miniSqlValue!) - // logger.log("Sucessfully set timezone with id \(timeZoneId) result \(result)") - // } catch { - // logger.log("Error while setting timezone") - // } - // } - } + private func getProToken() { + sessionAsyncHandler.async { + do { + var error: NSError? + let proToken = try self.model.getToken(&error) + DispatchQueue.main.async { + if proToken != nil && proToken != "" { + Constants.appGroupDefaults.set(proToken, forKey: Constants.proToken) + logger.log("Sucessfully got protoken \(proToken)") + } else { + logger.log("Failed to get user id") + } + } + } catch { + DispatchQueue.main.async { + SentryUtils.caputure(error: error as NSError) + } + } + } -// public func configDirFor(suffix: String) -> String { -// let filesDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) -// .first! -// -// let fileURL = filesDirectory.appendingPathComponent(".lantern" + suffix) -// -// if !FileManager.default.fileExists(atPath: fileURL.path) { -// do { -// try FileManager.default.createDirectory( -// at: fileURL, withIntermediateDirectories: true, attributes: nil) -// } catch { -// -// print(error.localizedDescription) -// SentryUtils.caputure(error: error as NSError) -// } -// } -// return fileURL.path -// } -} + } -class Settings: NSObject, InternalsdkSettingsProtocol { - func getHttpProxyHost() -> String { - return "127.0.0.1" + func observeStatsUpdates() { + logger.debug("observesing stats udpates") + Constants.appGroupDefaults.addObserver( + self, forKeyPath: Constants.statsData, options: [.new], context: nil) } - func getHttpProxyPort() -> Int { - return 49125 + // System method that observe value user default path + override func observeValue( + forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, + context: UnsafeMutableRawPointer? + ) { + logger.debug("observeValue call with key \(keyPath)") + if keyPath == Constants.statsData { + logger.debug("Message comming from tunnel") + if let statsData = change![.newKey] as? Data { + updateStats(stats: statsData) + } + } } - func stickyConfig() -> Bool { - return false + func updateStats(stats: Data) { + do { + // Convert the JSON data back to a dictionary + if let dataDict = try JSONSerialization.jsonObject(with: stats, options: []) + as? [String: Any] + { + try invoke( + "updateStats", arguments: dataDict, completion: emptyCompletion) + logger.debug("updateStats data received: \(dataDict)") + } + } catch { + logger.debug("Failed to deserialize JSON data: \(error)") + } } - func timeoutMillis() -> Int { - return 60000 + deinit { + // Remove observer when the observer is deallocated + Constants.appGroupDefaults.removeObserver(self, forKeyPath: Constants.statsData) } } diff --git a/ios/Runner/Lantern/Models/VpnModel.swift b/ios/Runner/Lantern/Models/VpnModel.swift index e6b86677e..5faf5ab97 100644 --- a/ios/Runner/Lantern/Models/VpnModel.swift +++ b/ios/Runner/Lantern/Models/VpnModel.swift @@ -25,6 +25,7 @@ class VpnModel: BaseModel, InternalsdkVPNManagerProtocol { self.sessionModel = sessionModel try super.init(flutterBinary, model) model.setManager(self) + } private func saveVPNStatus(status: String) { diff --git a/ios/Runner/Lantern/Utils/Constants.swift b/ios/Runner/Lantern/Utils/Constants.swift index 2fa2aa695..f524a5c95 100644 --- a/ios/Runner/Lantern/Utils/Constants.swift +++ b/ios/Runner/Lantern/Utils/Constants.swift @@ -14,6 +14,7 @@ struct Constants { static let appBundleId = "org.getlantern.lantern" static let netExBundleId = "org.getlantern.lantern.Tunnel" static let appGroupName = "group.getlantern.lantern" + static let statsData = "statsData" // MARK: App Group static let appGroupDefaults = UserDefaults(suiteName: appGroupName)! static var appGroupContainerURL: URL { @@ -21,10 +22,10 @@ struct Constants { return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)! } - // Create lantern dir at start all other sub folder can create by other service - // All folder creation should happnen at only once place + // Create lantern dir at start all other sub folder can create by other service + // All folder creation should happnen at only once place static var lanternDirectory: URL { - + return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) .first!.appendingPathComponent(".lanternservice") } diff --git a/ios/Tunnel/LanternAdapter.swift b/ios/Tunnel/LanternAdapter.swift new file mode 100644 index 000000000..8e787f535 --- /dev/null +++ b/ios/Tunnel/LanternAdapter.swift @@ -0,0 +1,26 @@ +import Foundation +import NetworkExtension + +public class LanternAdapter { + public typealias LogHandler = (String) -> Void + + /// Packet tunnel provider. + private weak var packetTunnelProvider: NEPacketTunnelProvider? + + /// Log handler closure. + private let logHandler: LogHandler + + /// - Parameter packetTunnelProvider: an instance of `NEPacketTunnelProvider`. Internally stored + /// as a weak reference. + /// - Parameter logHandler: a log handler closure. + public init(with packetTunnelProvider: NEPacketTunnelProvider, logHandler: @escaping LogHandler) { + self.packetTunnelProvider = packetTunnelProvider + self.logHandler = logHandler + } + + /// Tunnel device file descriptor. + public var tunnelFileDescriptor: Int32 { + return self.packetTunnelProvider?.packetFlow.value(forKeyPath: "socket.fileDescriptor") + as! Int32 + } +} diff --git a/ios/Tunnel/PacketTunnelProvider.swift b/ios/Tunnel/PacketTunnelProvider.swift index 30d3bb369..9a0bfff85 100644 --- a/ios/Tunnel/PacketTunnelProvider.swift +++ b/ios/Tunnel/PacketTunnelProvider.swift @@ -120,7 +120,7 @@ extension PacketTunnelProvider { var error: NSError? welf.client = IosClient( welf, UDPDialer(), MemChecker(), welf.constants.configDirectoryURL.path, welf.mtu, - Constants.capturedDNSHost, Constants.realDNSHost, &error) + Constants.capturedDNSHost, Constants.realDNSHost, StatsTracker(), &error) if let err = error { logger.error(err.localizedDescription) diff --git a/ios/Tunnel/StatsTracker.swift b/ios/Tunnel/StatsTracker.swift new file mode 100644 index 000000000..3318eafe9 --- /dev/null +++ b/ios/Tunnel/StatsTracker.swift @@ -0,0 +1,37 @@ +// +// StatsTracker.swift +// Tunnel +// +// Created by jigar fumakiya on 23/05/24. +// + +import Foundation +import Internalsdk + +// StatsTracker serves as an abstraction for tracking statistics relevant to the VPN connection. +// It is responsible for updating statistics data received from the server when the user connects to the VPN. +// The updated statistics data is stored in a shared container accessible across the application, +// facilitating data exchange between different components depending on the VPN status. +class StatsTracker: NSObject, IosStatsTrackerProtocol { + func updateStats(_ p0: String?, p1: String?, p2: String?, p3: Int, p4: Int, p5: Bool) { + logger.debug( + "UpdateStats city \(p0) country \(p1) servercountrycode \(p3) hasSucceedingProxy \(p5)") + //Save data comming from server + let dataDict: [String: Any] = [ + "city": p0 ?? "", + "country": p1 ?? "", + "serverCountryCode": p2 ?? "", + "httpsUpgrades": p3, + "adsBlocked": p4, + "hasSucceedingProxy": p5, + ] + do { + // Convert the dictionary to JSON data + let jsonData = try JSONSerialization.data(withJSONObject: dataDict, options: []) + Constants.appGroupDefaults.set(jsonData, forKey: Constants.statsData) + logger.debug("Stats data has been saved") + } catch { + logger.error("Failed to serialize stats data to JSON: \(error)") + } + } +} diff --git a/lib/account/device_linking/approve_device.dart b/lib/account/device_linking/approve_device.dart index 4bd7fe4a0..9411c03da 100644 --- a/lib/account/device_linking/approve_device.dart +++ b/lib/account/device_linking/approve_device.dart @@ -1,5 +1,6 @@ import 'package:lantern/common/common.dart'; import 'package:lantern/plans/utils.dart'; + import 'explanation_step.dart'; @RoutePage(name: 'ApproveDevice') diff --git a/lib/account/language.dart b/lib/account/language.dart index fa7a9ef78..cf6fc0a59 100644 --- a/lib/account/language.dart +++ b/lib/account/language.dart @@ -1,11 +1,10 @@ import 'package:intl/intl.dart'; import 'package:lantern/common/common.dart'; import 'package:lantern/i18n/localization_constants.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; @RoutePage(name: 'Language') class Language extends StatelessWidget { - Language({Key? key}) : super(key: key); + const Language({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -15,7 +14,7 @@ class Language extends StatelessWidget { body: sessionModel .language((BuildContext context, String currentLang, Widget? child) { // Splint language by just code - final countryCode= currentLang; + final countryCode= checkSupportedLanguages(currentLang) ; return ListView.builder( itemCount: languages.length, itemBuilder: (BuildContext context, int index) { diff --git a/lib/account/settings.dart b/lib/account/settings.dart index 4736bc04c..1cdead378 100644 --- a/lib/account/settings.dart +++ b/lib/account/settings.dart @@ -1,15 +1,12 @@ -import 'package:catcher_2/core/catcher_2.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:intl/intl.dart'; import 'package:lantern/common/app_methods.dart'; -import 'package:lantern/common/app_secret.dart'; import 'package:lantern/common/common.dart'; import 'package:lantern/common/common_desktop.dart'; import 'package:lantern/common/ui/app_loading_dialog.dart'; import 'package:lantern/i18n/localization_constants.dart'; import 'package:lantern/messaging/messaging_model.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @RoutePage(name: 'Settings') class Settings extends StatelessWidget { diff --git a/lib/account/support.dart b/lib/account/support.dart index 022ab72e6..d66eb1afa 100644 --- a/lib/account/support.dart +++ b/lib/account/support.dart @@ -1,4 +1,5 @@ import 'package:url_launcher/url_launcher.dart'; + import '../common/common.dart'; @RoutePage(name: 'Support') diff --git a/lib/app.dart b/lib/app.dart index 3f8bdeaa4..89b91cd77 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -123,7 +123,7 @@ class _LanternAppState extends State { } return sessionModel.language( (context, lang, child) { - Localization.locale = lang; + Localization.locale = lang.startsWith('en')?'en_us':lang; return GlobalLoaderOverlay( useDefaultLoading: false, overlayColor: Colors.black.withOpacity(0.5), diff --git a/lib/catcher_setup.dart b/lib/catcher_setup.dart index ad89850ea..f3c6cbfae 100644 --- a/lib/catcher_setup.dart +++ b/lib/catcher_setup.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:catcher_2/catcher_2.dart'; import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/lib/common/common.dart b/lib/common/common.dart index 154be3e54..71883d4d7 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -33,6 +33,7 @@ export 'package:stop_watch_timer/stop_watch_timer.dart'; export 'add_nonbreaking_spaces.dart'; export 'app_keys.dart'; +export 'app_secret.dart'; export 'disable_back_button.dart'; export 'iterable_extension.dart'; export 'list_subscriber.dart'; @@ -51,7 +52,6 @@ export 'ui/continue_arrow.dart'; export 'ui/copy_text.dart'; export 'ui/countdown_min_sec.dart'; export 'ui/countdown_stopwatch.dart'; - // custom components export 'ui/custom/asset_image.dart'; export 'ui/custom/badge.dart'; @@ -62,10 +62,10 @@ export 'ui/custom/fullscreen_video_viewer.dart'; export 'ui/custom/fullscreen_viewer.dart'; export 'ui/custom/ink_well.dart'; export 'ui/custom/list_item_factory.dart'; +export 'ui/custom/retry_widget.dart'; export 'ui/custom/rounded_rectangle_border.dart'; export 'ui/custom/text.dart'; export 'ui/custom/text_field.dart'; -export 'ui/custom/retry_widget.dart'; export 'ui/dimens.dart'; export 'ui/focused_menu.dart'; export 'ui/full_screen_dialog.dart'; @@ -87,11 +87,9 @@ export 'ui/round_button.dart'; export 'ui/search_field.dart'; export 'ui/show_bottom_modal.dart'; export 'ui/show_snackbar.dart'; - export 'ui/text_highlighter.dart'; export 'ui/text_styles.dart'; export 'ui/transitions.dart'; -export 'app_secret.dart'; final appLogger = Logger( printer: PrettyPrinter( diff --git a/lib/common/common_desktop.dart b/lib/common/common_desktop.dart index e8c95fad7..cef3aa28b 100644 --- a/lib/common/common_desktop.dart +++ b/lib/common/common_desktop.dart @@ -1,18 +1,10 @@ -import 'package:lantern/common/common.dart'; -import 'package:tray_manager/tray_manager.dart'; -import 'package:lantern/ffi.dart'; -import 'package:ffi/ffi.dart'; -import 'dart:ffi'; -import 'package:lantern/common/ui/websocket.dart'; -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - export 'dart:convert'; export 'dart:ffi'; // For FFI + export 'package:ffi/ffi.dart'; export 'package:ffi/src/utf8.dart'; -export 'package:lantern/ffi.dart'; export 'package:lantern/common/tray_handler.dart'; export 'package:lantern/common/ui/websocket.dart'; +export 'package:lantern/ffi.dart'; export 'package:web_socket_channel/io.dart'; export 'package:web_socket_channel/web_socket_channel.dart'; diff --git a/lib/common/ffi_list_subscriber.dart b/lib/common/ffi_list_subscriber.dart index 0de3db544..36d290ced 100644 --- a/lib/common/ffi_list_subscriber.dart +++ b/lib/common/ffi_list_subscriber.dart @@ -2,10 +2,6 @@ import 'dart:collection'; import 'common.dart'; import 'common_desktop.dart'; -import 'list_subscriber.dart'; -import 'package:lantern/i18n/i18n.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:intl/intl.dart'; class FfiListNotifier extends SubscribedNotifier> { FfiListNotifier( diff --git a/lib/common/ffi_subscriber.dart b/lib/common/ffi_subscriber.dart index c50743d45..26271b42f 100644 --- a/lib/common/ffi_subscriber.dart +++ b/lib/common/ffi_subscriber.dart @@ -1,9 +1,7 @@ +import 'package:web_socket_channel/status.dart' as status; + import 'common.dart'; import 'common_desktop.dart'; -import 'dart:convert'; -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/status.dart' as status; -import 'package:web_socket_channel/web_socket_channel.dart'; extension BoolParsing on String { bool parseBool() { diff --git a/lib/common/model.dart b/lib/common/model.dart index 504841ae1..0472643b6 100644 --- a/lib/common/model.dart +++ b/lib/common/model.dart @@ -1,10 +1,10 @@ import 'dart:collection'; -import 'package:lantern/common/ffi_subscriber.dart'; import 'package:lantern/common/ffi_list_subscriber.dart'; +import 'package:lantern/common/ffi_subscriber.dart'; import 'package:lantern/messaging/messaging.dart'; + import 'common_desktop.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; abstract class Model { late MethodChannel methodChannel; diff --git a/lib/common/tray_handler.dart b/lib/common/tray_handler.dart index 19d8b68dd..84825e2d6 100644 --- a/lib/common/tray_handler.dart +++ b/lib/common/tray_handler.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:lantern/common/common.dart'; import 'package:lantern/common/common_desktop.dart'; import 'package:tray_manager/tray_manager.dart'; diff --git a/lib/common/ui/app_webview.dart b/lib/common/ui/app_webview.dart index 9b9cefc5c..df88cf6f2 100644 --- a/lib/common/ui/app_webview.dart +++ b/lib/common/ui/app_webview.dart @@ -1,7 +1,6 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_windows_webview/flutter_windows_webview.dart'; import 'package:lantern/common/common.dart'; -import 'package:lantern/common/ui/app_loading_dialog.dart'; @RoutePage(name: 'AppWebview') class AppWebView extends StatefulWidget { diff --git a/lib/common/ui/custom/text.dart b/lib/common/ui/custom/text.dart index eabe25901..23107fbea 100644 --- a/lib/common/ui/custom/text.dart +++ b/lib/common/ui/custom/text.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:lantern/common/common.dart'; class CText extends StatelessWidget { diff --git a/lib/common/ui/websocket.dart b/lib/common/ui/websocket.dart index 0be4e530b..116b7a0f5 100644 --- a/lib/common/ui/websocket.dart +++ b/lib/common/ui/websocket.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'dart:convert'; -import 'package:flutter/foundation.dart'; + import 'package:lantern/common/common_desktop.dart'; abstract class WebsocketService { diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index 56039368c..02d265aac 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -1,7 +1,4 @@ -import 'package:auto_route/auto_route.dart'; import 'package:lantern/common/common.dart'; -import 'package:lantern/common/ui/transitions.dart'; -import 'package:lantern/core/router/router.gr.dart'; @AutoRouterConfig( replaceInRouteName: 'Page,Route,Screen', diff --git a/lib/home.dart b/lib/home.dart index c99dd2568..b2956bcb7 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -47,23 +47,27 @@ class _HomePageState extends State with TrayListener, WindowListener { } void channelListener() { + if(Platform.isIOS) return; + const mainMethodChannel = MethodChannel('lantern_method_channel'); const navigationChannel = MethodChannel('navigation'); - sessionModel.getChatEnabled().then((chatEnabled) { - if (chatEnabled) { - messagingModel - .shouldShowTryLanternChatModal() - .then((shouldShowModal) async { - if (shouldShowModal) { - // open VPN tab - sessionModel.setSelectedTab(context,TAB_VPN); - // show Try Lantern Chat dialog - await context.router - .push(FullScreenDialogPage(widget: TryLanternChat())); - } - }); - } - }); + if (Platform.isAndroid) { + sessionModel.getChatEnabled().then((chatEnabled) { + if (chatEnabled) { + messagingModel + .shouldShowTryLanternChatModal() + .then((shouldShowModal) async { + if (shouldShowModal) { + // open VPN tab + sessionModel.setSelectedTab(context,TAB_VPN); + // show Try Lantern Chat dialog + await context.router + .push(FullScreenDialogPage(widget: TryLanternChat())); + } + }); + } + }); + } navigationChannel.setMethodCallHandler(_handleNativeNavigationRequest); // Let back-end know that we're ready to handle navigation diff --git a/lib/i18n/i18n.dart b/lib/i18n/i18n.dart index 3ae2e7b98..a24578b83 100644 --- a/lib/i18n/i18n.dart +++ b/lib/i18n/i18n.dart @@ -1,5 +1,5 @@ -import 'package:i18n_extension_importer/src/io/import.dart'; import 'package:i18n_extension/i18n_extension.dart'; +import 'package:i18n_extension_importer/src/io/import.dart'; import 'package:lantern/common/common.dart'; extension Localization on String { diff --git a/lib/i18n/localization_constants.dart b/lib/i18n/localization_constants.dart index 73a10c396..9624b464b 100644 --- a/lib/i18n/localization_constants.dart +++ b/lib/i18n/localization_constants.dart @@ -18,6 +18,13 @@ const languages = [ 'bn_BD', ]; +String checkSupportedLanguages(String language){ + if(languages.contains(language)){ + return language; + } + return 'en_Us'; +} + String displayLanguage(String languageCode) { if (languageCode == 'ar_EG') { return 'العربية'; diff --git a/lib/messaging/messaging_model.dart b/lib/messaging/messaging_model.dart index c16236b58..b9e97ba65 100644 --- a/lib/messaging/messaging_model.dart +++ b/lib/messaging/messaging_model.dart @@ -1,7 +1,8 @@ // import 'calls/signaling.dart'; -import 'messaging.dart'; import 'package:lantern/common/common_desktop.dart'; +import 'messaging.dart'; + final messagingModel = MessagingModel(); class MessagingModel extends Model { diff --git a/lib/messaging/protos_flutteronly/messaging.pbenum.dart b/lib/messaging/protos_flutteronly/messaging.pbenum.dart index 2941f5be8..9b52e6f35 100644 --- a/lib/messaging/protos_flutteronly/messaging.pbenum.dart +++ b/lib/messaging/protos_flutteronly/messaging.pbenum.dart @@ -7,6 +7,7 @@ // ignore_for_file: UNDEFINED_SHOWN_NAME import 'dart:core' as $core; + import 'package:protobuf/protobuf.dart' as $pb; class MessageDirection extends $pb.ProtobufEnum { diff --git a/lib/messaging/protos_flutteronly/messaging.pbjson.dart b/lib/messaging/protos_flutteronly/messaging.pbjson.dart index 7a83913f0..3e5ead132 100644 --- a/lib/messaging/protos_flutteronly/messaging.pbjson.dart +++ b/lib/messaging/protos_flutteronly/messaging.pbjson.dart @@ -1,4 +1,5 @@ /// +import 'dart:convert' as $convert; // Generated code. Do not modify. // source: protos_flutteronly/messaging.proto // @@ -6,7 +7,6 @@ // ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name import 'dart:core' as $core; -import 'dart:convert' as $convert; import 'dart:typed_data' as $typed_data; @$core.Deprecated('Use messageDirectionDescriptor instead') const MessageDirection$json = const { diff --git a/lib/plans/plan_details.dart b/lib/plans/plan_details.dart index 7f0793b0d..c4077bbed 100644 --- a/lib/plans/plan_details.dart +++ b/lib/plans/plan_details.dart @@ -1,5 +1,4 @@ import 'package:lantern/common/common.dart'; -import 'package:lantern/plans/payment_provider.dart'; import 'package:lantern/plans/utils.dart'; class PlanCard extends StatelessWidget { diff --git a/lib/plans/reseller_checkout.dart b/lib/plans/reseller_checkout.dart index 4d3c77eb2..60f3e4a02 100644 --- a/lib/plans/reseller_checkout.dart +++ b/lib/plans/reseller_checkout.dart @@ -1,9 +1,9 @@ import 'package:email_validator/email_validator.dart'; +import 'package:intl/intl.dart'; import 'package:lantern/common/common.dart'; import 'package:lantern/plans/plan_details.dart'; import 'package:lantern/plans/tos.dart'; import 'package:lantern/plans/utils.dart'; -import 'package:intl/intl.dart'; class ResellerCodeFormatter extends TextInputFormatter { @override diff --git a/lib/plans/utils.dart b/lib/plans/utils.dart index e53b33888..b8a5bfe40 100644 --- a/lib/plans/utils.dart +++ b/lib/plans/utils.dart @@ -1,7 +1,6 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:lantern/common/common.dart'; import 'package:lantern/common/ui/app_webview.dart'; -import 'package:lantern/plans/payment_provider.dart'; const defaultTimeoutDuration = Duration(seconds: 10); diff --git a/lib/replica/logic/uploader.dart b/lib/replica/logic/uploader.dart index 43d4d961f..7196824c5 100644 --- a/lib/replica/logic/uploader.dart +++ b/lib/replica/logic/uploader.dart @@ -1,7 +1,7 @@ import 'package:flutter_uploader/flutter_uploader.dart'; +import 'package:http/http.dart' as http; import 'package:lantern/common/common.dart'; import 'package:lantern/messaging/notifications.dart'; -import 'package:http/http.dart' as http; /// ReplicaUploader is a singleton class. Use it like this: /// - Initialize ReplicaUploader by calling ReplicaUploader.inst.init() diff --git a/lib/vpn/vpn_model.dart b/lib/vpn/vpn_model.dart index c5698b42a..88f8547d2 100644 --- a/lib/vpn/vpn_model.dart +++ b/lib/vpn/vpn_model.dart @@ -1,5 +1,5 @@ -import 'package:lantern/vpn/vpn.dart'; import 'package:lantern/common/common_desktop.dart'; +import 'package:lantern/vpn/vpn.dart'; final vpnModel = VpnModel(); diff --git a/lib/vpn/vpn_server_location.dart b/lib/vpn/vpn_server_location.dart index 34f15c737..c35a815af 100644 --- a/lib/vpn/vpn_server_location.dart +++ b/lib/vpn/vpn_server_location.dart @@ -1,6 +1,8 @@ import 'package:lantern/vpn/vpn.dart'; class ServerLocationWidget extends StatelessWidget { + const ServerLocationWidget({super.key}); + @override Widget build(BuildContext context) { return CInkWell( diff --git a/lib/vpn/vpn_status.dart b/lib/vpn/vpn_status.dart index c39987978..5f843fe37 100644 --- a/lib/vpn/vpn_status.dart +++ b/lib/vpn/vpn_status.dart @@ -1,5 +1,4 @@ import 'package:lantern/vpn/vpn.dart'; -import 'package:lantern/common/common_desktop.dart'; class VPNStatus extends StatelessWidget { @override diff --git a/lib/vpn/vpn_tab.dart b/lib/vpn/vpn_tab.dart index 2f88c5ac7..435222e93 100644 --- a/lib/vpn/vpn_tab.dart +++ b/lib/vpn/vpn_tab.dart @@ -1,6 +1,7 @@ import 'package:lantern/account/split_tunneling.dart'; import 'package:lantern/messaging/messaging.dart'; import 'package:lantern/vpn/vpn.dart'; + import 'vpn_bandwidth.dart'; import 'vpn_pro_banner.dart'; import 'vpn_server_location.dart'; @@ -8,7 +9,7 @@ import 'vpn_status.dart'; import 'vpn_switch.dart'; class VPNTab extends StatelessWidget { - VPNTab({Key? key}) : super(key: key); + const VPNTab({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -26,7 +27,7 @@ class VPNTab extends StatelessWidget { body: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (!proUser && !Platform.isIOS) ProBanner() else const SizedBox(), + if (!proUser && !Platform.isIOS) const ProBanner() else const SizedBox(), const VPNSwitch(), Container( padding: const EdgeInsetsDirectional.all(16), @@ -44,12 +45,12 @@ class VPNTab extends StatelessWidget { children: [ VPNStatus(), const CDivider(height: 32.0), - ServerLocationWidget(), + const ServerLocationWidget(), if (Platform.isAndroid) ...{ const CDivider(height: 32.0), SplitTunnelingWidget(), }, - if (!proUser) const VPNBandwidth(), + if (!proUser&& Platform.isAndroid) const VPNBandwidth(), ], ), ), diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 042bc332e..25bc122e3 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -141,4 +141,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3