NEHotspotHelper: How to get a kNEHotspotHelperCommandTypeAuthenticate command

I'm trying to get the NEHotspotHelper to work with our captive portal. The appropriate entitlements are in place, the "network-authentication" background mode is set in the plist file, and the NEHotspotHelper handler is installed.


However, all I ever get in the handler are the commands kNEHotspotHelperCommandTypeFilterScanList and kNEHotspotHelperCommandTypeEvaluate.


When I get the kNEHotspotHelperCommandTypeEvaluate for a network I can handle, I return the following response:


NEHotspotHelperResponse *response = [cmd createResponse:kNEHotspotHelperResultSuccess];
[cmd.network setConfidence:kNEHotspotHelperConfidenceHigh];
[response setNetwork:cmd.network];
[response deliver];


Should this not lead to the kNEHotspotHelperCommandTypeAuthenticate command being issued against my handler? If not, what do I have to do to get the authenticate command?


Right now, I don't get the authenticate command, and the captive portal still pops up.


Thanks for any help,

Matthias

Replies

Should this not lead to the kNEHotspotHelperCommandTypeAuthenticate command being issued against my handler?

Yes, it should. Resolving a

.Evaluate
command with anything other than a
.Low
confidence should result in the hotspot state machine following the Captive transition from the Evaluating state to the Authenticating state (this is with reference to the hotspot state machine, see Figure 1-1 of the Hotspot Network Subsystem Programming Guide). That’s the behaviour I saw when I last tested this. I’m not sure what’s happening in your case.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hello Eskimo,


Thanks for your fast reply. I add some more code to my response, and some more details on the problem.


-(void)initHotspotHelper {
    NSMutableDictionary* options = [[NSMutableDictionary alloc] init];
    [options setObject:@"Connect with MyWIFI" forKey:kNEHotspotHelperOptionDisplayName];
    dispatch_queue_t queue = dispatch_queue_create("HotspotQueue", 0);
    [NEHotspotHelper registerWithOptions:options queue:queue handler: ^(NEHotspotHelperCommand * cmd) {
        NSLog(@"Hotspot command: %ld", (long)cmd.commandType);
        if(cmd.commandType == kNEHotspotHelperCommandTypeEvaluate)
        {
            if(cmd.network && cmd.network.SSID)
            {
                if([currentSsid rangeOfString:@"MyWIFI"].location != NSNotFound)
                {
                    NSLog(@"Associated network: %@ / %d", cmd.network.SSID, cmd.network.isChosenHelper);
                    NEHotspotHelperResponse *response = [cmd createResponse:kNEHotspotHelperResultSuccess];
                    [cmd.network setConfidence:kNEHotspotHelperConfidenceHigh];
                    [response setNetwork:cmd.network];
                    [response deliver];
                }
            }
        }
        else if (cmd.commandType == kNEHotspotHelperCommandTypeFilterScanList)
        {
            NSMutableArray* responseArray = [NSMutableArray array];
            for (NEHotspotNetwork* network  in cmd.networkList)
            {
                if(!network.SSID) continue;
  
                NSString* ssid = [network.SSID stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  
                if ([ssid rangeOfString:@"MyWIFI"].location != NSNotFound)
                {
                    [network setConfidence:kNEHotspotHelperConfidenceHigh];
                    [responseArray addObject:network];
                }
  
            }
            // any networks found we cover? then return them
            if(responseArray.count > 0)
            {
                NEHotspotHelperResponse *response = [cmd createResponse:kNEHotspotHelperResultSuccess];
                [response setNetworkList:responseArray];
                [response deliver];
            }
        }
    }];
}


This is the whole function I use to react on HotspotHelper commands. It's called in the ApplicationDelegate's didFinishWithLaunching function.

What happens is:

  • The installed handler is called with the kNEHotspotHelperCommandTypeFilterScanList command when Settings -> Wifi is opened, and the appropriate networks are attributed with the "Connect with MyWIFI" text. The hotspot command logging at the beginning shows a command "1" for filter scan list. This works as expected.
  • When I choose a network from the list of networks, the installed handler is called with the kNEHotspotHelperCommandTypeEvaluate command. The hotspot command logging at the beginning shows a command "2" for evaluate. The cmd.network value is the selected network, and if it is a "MyWIFI" SSID I set the confidence and the network in the response. However, the cmd.network.isChosenHelper value always is false, even if it is a network that has the "Connect with MyWIFI" text on it and I returned a confidence of "High" for.
  • After I chose a MyWIFI network, I do not get an authenticate call in the handler. In fact, I never saw any other command in the handler than scan list or evaluate.

There are no other apps on the iPhone that claim any network handling (and I tried it on other iPhones as well), so why is my app never chosen as the one to handle the authentication? I suppose this is the base problem, as after the handler received the kNEHotspotHelperCommandTypeEvaluate command and I returned confidence "High" the captive portal pops up, which shouldn't happen, and the handler does not get the authenticate command, which should happen.


As written in my original post, the HotspotHelper entitlements are set, as well as the network-authentication background mode (I assume nothing would work if thoise are not present anyway). What I find really strange is that all seems to work as expected, just the step that the app gets chosen to handle the authentication is not happening.


Are there any other places in the app I have to do something to get this working? Or is there a mistake in the above code? Or could it be some setting / reaction on the router that can cause this wrong behaviour? There doesn't seem to be any more things the app can actively set on a NEHotspotHelper response, so right now I'm completely clueless on how to get it working or even debug the situation any further.

I have a couple of suggestions for you (see below) but I’m not confident that they will actually fix the problem. If you get stuck, I recommend that you open a DTS tech support incident so that I can spend more time looking at this.

My suggestions:

  • Rather than creating your own queue, use the main queue (

    dispatch_get_main_queue
    ). Ultimately it might make sense to move this work off to a secondary queue but, during bringup, it’s easier to run everything on the main queue.
  • Fix your handling of exceptional conditions. Your code has a lot of places where it checks that things are valid and does nothing if they’re not. This is a mistake. You must deliver exactly one response for ever command that you’re given. If the command is malformed, or your encounter some sort of failure, respond with

    .Failure
    .
  • A specific case of the previous point relates to how you’re handling the

    .FilterScanList
    command. You only deliver a response if
    responseArray
    is non-empty. That’s not good. You need to respond to every
    .FilterScanList
    command, even if the filtered list is empty.

Finally, you should take a look at this post, which explains which fields of the command and response are meaningful for which commands.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks again for your answer and the valuable suggestions - I wasn't aware that the response always had to be delivered.


I made the changes to my code, but as you already assumed (and I also suspected) it did not resolve the issue. In our environment the "failure cases" were also not applicable, so in fact nothing much changed with the new code except the queue it's running on.


The base problem stays that the hotspot helper does not get the authenticate command, and after looking at the state machine again I suspect that the iOS Hotspot Helper in "Sequence When Network Is Captive -> point 6" deems the network to not be captive and therefore does not choose a best_helper and does not send an authenticate command.


I assume the statement "Each Hotspot Helper processes the

Evaluate
command and determines whether the network is captive (it requires authentication)."

means that the iOS internal Hotspot Helper determines the network captivity, as the Hotspot Helper handler in my code does not have any way to set this, it can only set the confidence level of handling the network?


One last question before we open a support incident:


Could the cause of the problem be that the hotspot helper deems the network not to be captive? And why would it think that, as clearly the router will pop up a captive portal after a few seconds? Is there some setting / configuration the router can influence so the Hotspot Helper sees it as captive and will choose a best_helper and issue an authenticate command?


… the Hotspot Helper handler in my code does not have any way to set this, it can only set the confidence level of handling the network?

With reference to Figure 1-1, the links out of the Evaluating state work as follows:

  • Not Captive if the response of the

    .Evaluate
    command is
    .None
  • Captive otherwise (that is, for

    .Low
    and
    .High
    )

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I am facing the same issues in my swift code as below


NEHotspotHelperCommandType.authenticate never called. Please help if you able to find the solution and, I want to handle the captive login with in the app for that I have a webview which can load the html for captive portal but as wifi is not connected webview is not loading. How can we make wifi to be auto login false so that wifi network works without the internet. I have seen this in a app which force the network to auto login false and that is how they able to open the captive portal.


Also when I check isChosenHelper in evaluation command, it always returns false even if I have set the confidence of that network high in filterScanList.



public func getAllWifi(){
      
        var options = [String: NSObject]()
        options[kNEHotspotHelperOptionDisplayName] = NSLocalizedString("linq verified", comment: "") as NSObject?
      
        NSLog("Lets register", "")
        NEHotspotHelper.register(options: options, queue: DispatchQueue.main, handler: {(_ cmd: NEHotspotHelperCommand) -> Void in
          
            if  cmd.commandType == NEHotspotHelperCommandType.filterScanList {
                /
              
                if cmd.networkList != nil {
                    self.networkList = []
                    var i2e1Network : [NEHotspotNetwork] = []
                    for network: NEHotspotNetwork in cmd.networkList! {
                      
                        NSLog("Found network \(network.bssid) with \(network.ssid)", "")
                        self.networkList.append(network)
                      
/
                            network.setConfidence(.high)
                            i2e1Network.append(network)
/
                    }
                  
                    let response = cmd.createResponse(NEHotspotHelperResult.success)
                    response.setNetworkList(i2e1Network)
                    response.deliver()
       
                  
                    /
                    self.categoriesNetworkList()
                  
                    /
                    self.numberOfCells = self.easyConnectNetworkList.count + self.otherNetworkList.count + self.swAppNetworkList.count + self.headerPosition + 1
                  
                    /
                    self.setUI()
                }else if cmd.network != nil{
                    let resp : NEHotspotHelperResponse = cmd.createResponse(NEHotspotHelperResult.success)
                    print(resp)
                  
                }
            }
            else if cmd.commandType == NEHotspotHelperCommandType.evaluate{
                /
                print("evaluting the network")
/
              
                if (cmd.network?.isChosenHelper)!{
                    print("isChosenHelper true");
                }else{
                    print("isChosenHelper false");
                }
          
              
                self.checkCaptive(cmd: cmd, completionBlock: {(isCaptive) -> Void in
                    if isCaptive{
                        let storyboard = UIStoryboard(name: "GetWifi", bundle: nil)
                        let getWifiVC = storyboard.instantiateViewController(withIdentifier: "CaptivePortalLoginViewController") as! CaptivePortalLoginViewController
/
                        self.present(getWifiVC, animated: true, completion: nil)
                      
                        cmd.network?.setConfidence(.high)
                     
                        let response = cmd.createResponse(NEHotspotHelperResult.success)
                        response.setNetwork(cmd.network!)
                        response.deliver()
                    }else{
                      
                        cmd.network?.setConfidence(.high)
                      
                        let response = cmd.createResponse(NEHotspotHelperResult.success)
                        response.setNetwork(cmd.network!)
                        response.deliver()
                        self.wifiConnected()
                    }
                })
            }
            else if cmd.commandType == NEHotspotHelperCommandType.authenticate{
                /
                print("authenticating the network")
                print("network::"+(cmd.network?.ssid)!)
              
                cmd.network?.setConfidence(.high)
                let response = cmd.createResponse(NEHotspotHelperResult.success)
                response.setNetwork(cmd.network!)
                response.deliver()
            }else if cmd.commandType == NEHotspotHelperCommandType.presentUI{
                 print("presentUI")
              
                cmd.network?.setConfidence(.high)
                let response = cmd.createResponse(NEHotspotHelperResult.success)
                response.setNetwork(cmd.network!)
                response.deliver()
            }else if cmd.commandType == NEHotspotHelperCommandType.maintain{
                 print("maintain")
            }
          
        })
    }