How to deactivate on demand connect VPN from network extension? - ios

I have configured an always on VPN with a NEOnDemandRuleConnect I retrieve some user data from a backend such as expiration date if the user has paid the subscription. If it expires I'd like to deactivate the VPN without opening the main app, doing it from the Network Extension. I retrieve the data from the backend using a daily timer and then check if the subscription has expired. Then I'd have a function that loads the VPN manager from the system settings app and then deactivate it and finally save it. If I don't deactivate the manager the device will be without connection as it's a VPN that has been configured to connect always with the NEOnDemandRule. The function will be more or less this one
func stopProtection(completion: #escaping (Result<Void>) -> Void) {
NSLog("Called stopProtection")
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
if let error = error {
NSLog("[SUBS] ERROR \(error)")
}
if let managers = managers {
if managers.count > 0 {
let index = managers.firstIndex(where: { $0.localizedDescription == Constants.vpnBundleId })
guard let index = index else {
completion(.error(ProtectionServiceError.noKidsVpnInstalled))
return
}
let myManager = managers[index]
myManager.loadFromPreferences(completionHandler: { (error) in
guard error == nil else {
completion(.error(ProtectionServiceError.errorStoppingTunnel))
return
}
// Deactivate the VPN and save it
myManager.isEnabled = false
myManager.saveToPreferences(completionHandler: { (error) in
guard error == nil else {
completion(.error(ProtectionServiceError.errorStoppingTunnel))
return
}
completion(.success(()))
})
})
} else {
completion(.error(ProtectionServiceError.errorStoppingTunnel))
}
}
}
}
All this code and logic is being performed in the extension with all the limitations it supposes. Using the previous function I'd only get the first NSLog saying Called stopProtection but it doesn't load any manager. Calling this from the main target it'd work. I don't know if I can load and modify the manager from the extension or it's another way to do it.

Okay, I have debugged the network extension by attaching to the process and looking into the device Console and this error pops up,
NETunnelProviderManager objects cannot be instantiated from NEProvider processes
So nope, there's the answer!

Related

Why are previous views that have been popped from the stack still active

I have this truck driving app in swiftUI where I use fire base to log users in and out. The problem is that when I sign in with one user, and all it’s fire base functionalities are triggered, After I log out from the current user and into a new user, the functionality of the old user is still playing out. I think it might have something to do with the firebase functions being in an onAppear method. I am not sure though.
This is the firebase code. I dont think what Im querying has any relation to the solution so I wont explain it but if you think it does than please let me know.
.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]) { (_,_) in
}
myDrivers = []
getEmails()
db.collectionGroup("resources").whereField("control", isEqualTo: true).addSnapshotListener { (snapshot, err) in
if myDrivers.count == 0{}
else{
if err != nil{print("Error fetching motion status: \(err)")}
if ((snapshot?.documents.isEmpty) != nil){}
// Gets all the driverLocation documents
for doc in snapshot!.documents{
if myDrivers.contains(doc.reference.parent.parent!.documentID){
let isDriving = doc.get("isDriving") as! Bool
let isStopped = doc.get("isStopped") as! Bool
let notified = doc.get("notified") as! Bool
if (isDriving == false && isStopped == false) || notified{}
else{
// Gets the name of the drivers
doc.reference.parent.parent?.getDocument(completion: { (snapshot, err) in
if err != nil{print("Error parsing driver name: \(err)")}
let firstName = snapshot?.get("firstName") as! String
let lastName = snapshot?.get("lastName") as! String
self.notifiedDriver = "\(firstName) \(lastName)"
})
// Logic
if isDriving{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) is back on the road.")
showDrivingToast.toggle()
doc.reference.updateData(["notified" : true])
}else if isStopped{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) has stopped driving.")
showStoppedToast.toggle()
doc.reference.updateData(["notified" : true])
}
}
}
else{}
}
}
}
})
Listeners don't die when a view controller is left. They remain active.
It's important to manage them through handlers for specific listeners as a view closes or the user navigates away. Here's how to remove a specific listener
let listener = db.collection("cities").addSnapshotListener { querySnapshot, error in
// ...
}
and then later
// Stop listening to changes
listener.remove()
Or, if your user is logging out, you can use removeAllObservers (for the RealtimeDatabase) to remove them all at one time, noting that
removeAllObservers must be called again for each child reference where
a listener was established
For Firestore, store the listeners in a listener array class var and when you want to remove them all, just iterate over the array elements calling .remove() on each.
There's additional info in the Firebase Getting Started Guide Detach Listener

Memory leak using Firebase

I execute an API call in Firebase for retrieving the user profile information and storing it in a ViewController member variable.
The API is declared as a static function inside a class MyApi:
// Get User Profile
static func getUserProfile(byID userId:String,response:#escaping (_ result:[User]?,_ error:Error?)->()) {
// check ID is valid
guard userId.length > 0 else {
print("Error retrieving Creator data: invalid user id provided")
response(nil,ApiErrors.invalidParameters)
return
}
// retrieve profile
let profilesNode = Database.database().reference().child(MyAPI.profilesNodeKey)
profilesNode.child(userId).observe(.value, with: { (snapshot) in
// check if a valid data structure is returned
guard var dictionary = snapshot.value as? [String:AnyObject] else {
print("Get User Profile API: cannot find request")
response([],nil)
return
}
// data mapping
dictionary["key"] = userId as AnyObject
guard let user = User(data:dictionary) else {
print("Get User Profile API: error mapping User profile data")
response(nil,ApiErrors.mappingError)
return
}
response([user], nil)
}) { (error) in
response(nil,ApiErrors.FirebaseError(description: error.localizedDescription))
}
}
and I call it like that:
MyAPI.getUserProfile(byID: creatorId) { (profiles, error) in
guard let profiles = profiles, profiles.count > 0 else {
Utility.showErrorBanner(message: "Error retrieving Creator profile")
print("Error retrieving creator profile ID:[\(creatorId)] \(String(describing: error?.localizedDescription))")
return
}
self.currentProfile = profiles.first!
}
The ViewController is called in Modal mode so it should be deallocated every time I exit the screen.
Problem: a huge chunk of memory get allocated when I enter the screen, but it doesn't get freed up when I leave it. I'm sure about this because the problem doesn't appear if I remove the line self.currentProfile = profiles.first! (obviously)
How can I avoid this from happening?
NOTE: currentProfile is of type User, which was used to be a struct. I made it a class so I could use a weak reference for storing the information:
weak var currentCreator: User? {
didSet {
updateView()
}
}
but the problem still persists.
You are adding an observer:
profilesNode.child(userId).observe(...)
But you never remove it. As long as that observe is still added, it will hold on to memory from the entire set of results, and continually retrieve new updates. It's a really bad practice not to remove your observers.
If you want to read data just a single time, there is a different API for that using observeSingleEvent.

Sync Realm Object Server connection completion with follow up realm object usage

I’m using Realm Object Server for a simple test project and I’m facing problems synchronizing ROS connection setup and follow up usage of the realm object to access the database.
In viewDidLoad I’m calling connectROS function to initialize realmRos object/connection:
var realmRos: Realm!
override func viewDidLoad() {
connectROS()
if(FBSDKAccessToken.current() != nil){
// logged in
getFBUserData()
}else{
// not logged in
print("didLoad, FB user not logged in")
}
}
func connectROS() {
let username = "realm-admin"
let password = "*********"
SyncUser.logIn(with: .usernamePassword(username: username, password: password, register: false), server: URL(string: "http://146.185.154.***:9080")!)
{ user, error in
print("ROS: checking user credentials")
if let user = user {
print("ROS: user credentials OK")
DispatchQueue.main.async {
// Opening a remote Realm
print("ROS: entering dispatch Q main async")
let realmURL = URL(string: "realm://146.185.154.***:9080/~/***book_test1")!
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: realmURL))
self.realmRos = try! Realm(configuration: config)
// Any changes made to this Realm will be synced across all devices!
}
} else if let error = error {
// handle error
print("ROS: user check FAIL")
fatalError(String(describing: error))
}
}
}
In viewDidLoad function next step is to get FB logged user (in this case I’m using FB authentication). After the logged FB user is fetched, the application perform check is that FB user is new user for my application and my proprietary ROS User’s table.
func checkForExistingProfile(user: User) -> Bool {
var userThatExist: User?
do {
try self.realmRos.write() {
userThatExist = self.realmRos.object(ofType: User.self, forPrimaryKey: user.userName)
}
} catch let error as NSError {
print("ROS is not connected")
print(error.localizedDescription)
}
if userThatExist != nil {
return true
} else {
return false
}
}
At this point checkForExistingProfile usually (not always) crashes at try self.realmRos.write() which happens to be nil.
I think the problem comes from the synchronization between connectROS execution (which is asynchrony) and checkForExistingProfile which execution comes before connectROS completion.
Since you didn't show how checkForExistingProfile() is called after viewDidLoad() this is conjecture, but based on everything else you described it's the likely cause.
What you need to do is not call checkForExistingProfile() until the sync user has logged in and your self.realmRos variable has been initialized. Cocoa Touch does nothing to automatically synchronize code written using an asynchronous pattern (like logIn(), which returns immediately but reports its actual completion state in a callback), so you need to manually ensure that whatever logIn() is supposed to do has been done before you call any additional code that depends on its completion.

CloudKit: Get users firstname/surname

I'm trying to get the users first name using cloud kit however the following code is not getting the users first name and is leaving firstNameFromFunction variable empty. Does anyone know how to achieve this in iOS 10?
let container = CKContainer.default()
container.fetchUserRecordID { (recordId, error) in
if error != nil {
print("Handle error)")
}else{
self.container.discoverUserInfo(
withUserRecordID: recordId!, completionHandler: { (userInfo, error) in
if error != nil {
print("Handle error")
}else{
if let userInfo = userInfo {
print("givenName = \(userInfo.displayContact?.givenName)")
print("familyName = \(userInfo.displayContact?.familyName)")
firstNameFromFunction = userInfo.displayContact?.givenName
}else{
print("no user info")
}
}
})
}
}
the permission screen that comes up when asking for the first time, IMO, is very poorly worded. They need to change that. It says "Allow people using 'your app' to look you up by email? People who know your email address will be able to see that you use this app." This make NO sense. This has nothing to do with asking the user to get their iCloud first name, last name, email address.
Speaking of email address - this and the phone number from the lookupInfo property is missing - i.e. set to nil, even though those values are legit and correct. Filing a bug tonight.
First, you will need to request permission to access the user's information.
Then, you can use a CKDiscoverUserIdentitiesOperation. This is just like any other CKOperation (eg. the modify record operation). You just need to create a new operation with the useridentitylookupinfo. Then you will also need to create a completion block to handle the results.
Here is an example function I created:
func getUserName(withRecordID recordID: CKRecordID,
completion: #escaping (String) -> ()) {
if #available(iOS 10.0, *) {
let userInfo = CKUserIdentityLookupInfo(userRecordID: recordID)
let discoverOperation = CKDiscoverUserIdentitiesOperation(userIdentityLookupInfos: [userInfo])
discoverOperation.userIdentityDiscoveredBlock = { (userIdentity, userIdentityLookupInfo) in
let userName = "\((userIdentity.nameComponents?.givenName ?? "")) \((userIdentity.nameComponents?.familyName ?? ""))"
completion(userName)
}
discoverOperation.completionBlock = {
completion("")
}
CKContainer.default().add(discoverOperation)
} else {
// iOS 10 and below version of the code above,
// no longer works. So, we just return an empty string.
completion("")
}
}
First you need to ask the user for permission to be discovered.
Use CKContainer.default().requestApplicationPermission method passing .userDiscoverability on applicationPermission parameter.
The CKContainer.default().discoverUserInfo method is deprecated on iOS 10. Instead use CKContainer.default().discoverUserIdentity method.
Do something like:
CKContainer.default().requestApplicationPermission(.userDiscoverability) { (status, error) in
CKContainer.default().fetchUserRecordID { (record, error) in
CKContainer.default().discoverUserIdentity(withUserRecordID: record!, completionHandler: { (userIdentity, error) in
print("\(userIdentity?.nameComponents?.givenName)")
print("\(userIdentity?.nameComponents?.familyName)")
})
}
}

How to identify iCloud logged in user in airplane mode?

I try to get userRecordID in airplane mode, but I get an error, any other way?
class func asdf() {
var defaultContainer = CKContainer.defaultContainer()
var publicDatabase = defaultContainer.publicCloudDatabase
defaultContainer.fetchUserRecordIDWithCompletionHandler({ userRecordID, error in
if error == nil {
println("userRecordID.recordName : \(userRecordID.recordName)")
} else {
println("\(error.localizedDescription)")
}
})
}
Terminal: Couldn't renew our secure session
I put an accountStatusWithCompletionHandler call outside of fetchUserRecordIDWithCompletionHandler, that returned CKAccountStatus.Available.
You cannot detect internet connectivity with CloudKit. It will only give you an error when there is no connectivity. If you do want to test for internet connectivity, then you could use the famous Reachability class like this: How to check for an active Internet connection on iOS or OSX?
If you want to detect changes to the iCloud account, then you can add the following code to your AppDelegate application didFinishLaunchingWithOptions:
var localeChangeObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSUbiquityIdentityDidChangeNotification, object: nil, queue: NSOperationQueue.mainQueue()) { _ in
println("The user’s iCloud login changed: should refresh all user data.")
}
If you then want to fetch the user id, you have to do a container.requestApplicationPermission to see if you are allowed to query an then a container.fetchUserRecordIDWithCompletionHandler. Bit this requires internet connection. You could cache it on the device together with the detection code above to get the correct status.
I came across to this code, comparing recently and previous logged in user's token, and if the same, use the previously downloaded userRecordID. The only problem that in some cases on my iPad ubiquityIdentityToken method returns nil even dow I am logged in, strange.
class func checkUser() {
let ubiquityIdentityToken = NSFileManager.defaultManager().ubiquityIdentityToken
let status = Utility.status()
let prevUbiquityIdentityToken = status.objectForKey("ubiquityIdentityToken")
if ubiquityIdentityToken != nil && ubiquityIdentityToken!.isEqual(prevUbiquityIdentityToken) {
} else if ubiquityIdentityToken != nil && !ubiquityIdentityToken!.isEqual(prevUbiquityIdentityToken) {
status.setObject(ubiquityIdentityToken!, forKey: "ubiquityIdentityToken")
Utility.saveStatus(status)
let defaultContainer = CKContainer.defaultContainer()
let publicDatabase = defaultContainer.publicCloudDatabase
defaultContainer.fetchUserRecordIDWithCompletionHandler({ userRecordID, error in
if error == nil {
//do some stuff
})
} else {
println("\(error.localizedDescription)")
}
})
} else {
//do some stuff
status.removeObjectForKey("ubiquityIdentityToken")
Utility.saveStatus(status)
}
}

Resources