Installing a configuration profile on iPhone - pro

2019-01-01 12:23发布

问题:

I would like to ship a configuration profile with my iPhone application, and install it if needed.

Mind you, we\'re talking about a configuration profile, not a provisioning profile.

First off, such a task is possible. If you place a config profile on a Web page and click on it from Safari, it will get installed. If you e-mail a profile and click the attachment, it will install as well. \"Installed\" in this case means \"The installation UI is invoked\" - but I could not even get that far.

So I was working under the theory that initiating a profile installation involves navigating to it as a URL. I added the profile to my app bundle.

A) First, I tried [sharedApp openURL] with the file:// URL into my bundle. No such luck - nothing happens.

B) I then added an HTML page to my bundle that has a link to the profile, and loaded it into a UIWebView. Clicking on the link does nothing. Loading an identical page from a Web server in Safari, however, works fine - the link is clickable, the profile installs. I provided a UIWebViewDelegate, answering YES to every navigation request - no difference.

C) Then I tried to load the same Web page from my bundle in Safari (using [sharedApp openURL] - nothing happens. I guess, Safari cannot see files inside my app bundle.

D) Uploading the page and the profile on a Web server is doable, but a pain on the organizational level, not to mention an extra source of failures (what if no 3G coverage? etc.).

So my big question is: **how do I install a profile programmatically?

And the little questions are: what can make a link non-clickable within a UIWebView? Is it possible to load a file:// URL from my bundle in Safari? If not, is there a local location on iPhone where I can place files and Safari can find them?

EDIT on B): the problem is somehow in the fact that we\'re linking to a profile. I renamed it from .mobileconfig to .xml (\'cause it\'s really XML), altered the link. And the link worked in my UIWebView. Renamed it back - same stuff. It looks as if UIWebView is reluctant to do application-wide stuff - since installation of the profile closes the app. I tried telling it that it\'s OK - by means of UIWebViewDelegate - but that did not convince. Same behavior for mailto: URLs within UIWebView.

For mailto: URLs the common technique is to translate them into [openURL] calls, but that doesn\'t quite work for my case, see scenario A.

For itms: URLs, however, UIWebView works as expected...

EDIT2: tried feeding a data URL to Safari via [openURL] - does not work, see here: iPhone Open DATA: Url In Safari

EDIT3: found a lot of info on how Safari does not support file:// URLs. UIWebView, however, very much does. Also, Safari on the simulator open them just fine. The latter bit is the most frustrating.


EDIT4: I never found a solution. Instead, I put together a two-bit Web interface where the users can order the profile e-mailed to them.

回答1:

1) Install a local server like RoutingHTTPServer

2) Configure the custom header :

[httpServer setDefaultHeader:@\"Content-Type\" value:@\"application/x-apple-aspen-config\"];

3) Configure the local root path for the mobileconfig file (Documents):

[httpServer setDocumentRoot:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]];

4) In order to allow time for the web server to send the file, add this :

Appdelegate.h

UIBackgroundTaskIdentifier bgTask;

Appdelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application {
    NSAssert(self->bgTask == UIBackgroundTaskInvalid, nil);
    bgTask = [application beginBackgroundTaskWithExpirationHandler: ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [application endBackgroundTask:self->bgTask];
            self->bgTask = UIBackgroundTaskInvalid;
        });
    }];
}

5) In your controller, call safari with the name of the mobileconfig stored in Documents :

[[UIApplication sharedApplication] openURL:[NSURL URLWithString: @\"http://localhost:12345/MyProfile.mobileconfig\"]];


回答2:

The answer from malinois worked for me, BUT, I wanted a solution that came back to the app automatically after the user installed the mobileconfig.

It took me 4 hours, but here is the solution, built on malinois\' idea of having a local http server: you return HTML to safari that refreshes itself; the first time the server returns the mobileconfig, and the second time it returns the custom url-scheme to get back to your app. The UX is what I wanted: the app calls safari, safari opens mobileconfig, when user hits \"done\" on mobileconfig, then safari loads your app again (custom url scheme).

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.

    _httpServer = [[RoutingHTTPServer alloc] init];
    [_httpServer setPort:8000];                               // TODO: make sure this port isn\'t already in use

    _firstTime = TRUE;
    [_httpServer handleMethod:@\"GET\" withPath:@\"/start\" target:self selector:@selector(handleMobileconfigRootRequest:withResponse:)];
    [_httpServer handleMethod:@\"GET\" withPath:@\"/load\" target:self selector:@selector(handleMobileconfigLoadRequest:withResponse:)];

    NSMutableString* path = [NSMutableString stringWithString:[[NSBundle mainBundle] bundlePath]];
    [path appendString:@\"/your.mobileconfig\"];
    _mobileconfigData = [NSData dataWithContentsOfFile:path];

    [_httpServer start:NULL];

    return YES;
}

- (void)handleMobileconfigRootRequest:(RouteRequest *)request withResponse:(RouteResponse *)response {
    NSLog(@\"handleMobileconfigRootRequest\");
    [response respondWithString:@\"<HTML><HEAD><title>Profile Install</title>\\
     </HEAD><script> \\
     function load() { window.location.href=\'http://localhost:8000/load/\'; } \\
     var int=self.setInterval(function(){load()},400); \\
     </script><BODY></BODY></HTML>\"];
}

- (void)handleMobileconfigLoadRequest:(RouteRequest *)request withResponse:(RouteResponse *)response {
    if( _firstTime ) {
        NSLog(@\"handleMobileconfigLoadRequest, first time\");
        _firstTime = FALSE;

        [response setHeader:@\"Content-Type\" value:@\"application/x-apple-aspen-config\"];
        [response respondWithData:_mobileconfigData];
    } else {
        NSLog(@\"handleMobileconfigLoadRequest, NOT first time\");
        [response setStatusCode:302]; // or 301
        [response setHeader:@\"Location\" value:@\"yourapp://custom/scheme\"];
    }
}

... and here is the code to call into this from the app (ie viewcontroller):

[[UIApplication sharedApplication] openURL:[NSURL URLWithString: @\"http://localhost:8000/start/\"]];

Hope this helps someone.



回答3:

I have written a class for installing a mobileconfig file via Safari and then returning to the app. It relies on the http server engine Swifter which I found to be working well. I want to share my code below for doing this. It is inspired by multiple code sources I found floating in the www. So if you find pieces of your own code, contributions to you.

class ConfigServer: NSObject {

    //TODO: Don\'t foget to add your custom app url scheme to info.plist if you have one!

    private enum ConfigState: Int
    {
        case Stopped, Ready, InstalledConfig, BackToApp
    }

    internal let listeningPort: in_port_t! = 8080
    internal var configName: String! = \"Profile install\"
    private var localServer: HttpServer!
    private var returnURL: String!
    private var configData: NSData!

    private var serverState: ConfigState = .Stopped
    private var startTime: NSDate!
    private var registeredForNotifications = false
    private var backgroundTask = UIBackgroundTaskInvalid

    deinit
    {
        unregisterFromNotifications()
    }

    init(configData: NSData, returnURL: String)
    {
        super.init()
        self.returnURL = returnURL
        self.configData = configData
        localServer = HttpServer()
        self.setupHandlers()
    }

    //MARK:- Control functions

    internal func start() -> Bool
    {
        let page = self.baseURL(\"start/\")
        let url: NSURL = NSURL(string: page)!
        if UIApplication.sharedApplication().canOpenURL(url) {
            var error: NSError?
            localServer.start(listeningPort, error: &error)
            if error == nil {
                startTime = NSDate()
                serverState = .Ready
                registerForNotifications()
                UIApplication.sharedApplication().openURL(url)
                return true
            } else {
                self.stop()
            }
        }
        return false
    }

    internal func stop()
    {
        if serverState != .Stopped {
            serverState = .Stopped
            unregisterFromNotifications()
        }
    }

    //MARK:- Private functions

    private func setupHandlers()
    {
        localServer[\"/start\"] = { request in
            if self.serverState == .Ready {
                let page = self.basePage(\"install/\")
                return .OK(.HTML(page))
            } else {
                return .NotFound
            }
        }
        localServer[\"/install\"] = { request in
            switch self.serverState {
            case .Stopped:
                return .NotFound
            case .Ready:
                self.serverState = .InstalledConfig
                return HttpResponse.RAW(200, \"OK\", [\"Content-Type\": \"application/x-apple-aspen-config\"], self.configData!)
            case .InstalledConfig:
                return .MovedPermanently(self.returnURL)
            case .BackToApp:
                let page = self.basePage(nil)
                return .OK(.HTML(page))
            }
        }
    }

    private func baseURL(pathComponent: String?) -> String
    {
        var page = \"http://localhost:\\(listeningPort)\"
        if let component = pathComponent {
            page += \"/\\(component)\"
        }
        return page
    }

    private func basePage(pathComponent: String?) -> String
    {
        var page = \"<!doctype html><html>\" + \"<head><meta charset=\'utf-8\'><title>\\(self.configName)</title></head>\"
        if let component = pathComponent {
            let script = \"function load() { window.location.href=\'\\(self.baseURL(component))\'; }window.setInterval(load, 600);\"
            page += \"<script>\\(script)</script>\"
        }
        page += \"<body></body></html>\"
        return page
    }

    private func returnedToApp() {
        if serverState != .Stopped {
            serverState = .BackToApp
            localServer.stop()
        }
        // Do whatever else you need to to
    }

    private func registerForNotifications() {
        if !registeredForNotifications {
            let notificationCenter = NSNotificationCenter.defaultCenter()
            notificationCenter.addObserver(self, selector: \"didEnterBackground:\", name: UIApplicationDidEnterBackgroundNotification, object: nil)
            notificationCenter.addObserver(self, selector: \"willEnterForeground:\", name: UIApplicationWillEnterForegroundNotification, object: nil)
            registeredForNotifications = true
        }
    }

    private func unregisterFromNotifications() {
        if registeredForNotifications {
            let notificationCenter = NSNotificationCenter.defaultCenter()
            notificationCenter.removeObserver(self, name: UIApplicationDidEnterBackgroundNotification, object: nil)
            notificationCenter.removeObserver(self, name: UIApplicationWillEnterForegroundNotification, object: nil)
            registeredForNotifications = false
        }
    }

    internal func didEnterBackground(notification: NSNotification) {
        if serverState != .Stopped {
            startBackgroundTask()
        }
    }

    internal func willEnterForeground(notification: NSNotification) {
        if backgroundTask != UIBackgroundTaskInvalid {
            stopBackgroundTask()
            returnedToApp()
        }
    }

    private func startBackgroundTask() {
        let application = UIApplication.sharedApplication()
        backgroundTask = application.beginBackgroundTaskWithExpirationHandler() {
            dispatch_async(dispatch_get_main_queue()) {
                self.stopBackgroundTask()
            }
        }
    }

    private func stopBackgroundTask() {
        if backgroundTask != UIBackgroundTaskInvalid {
            UIApplication.sharedApplication().endBackgroundTask(self.backgroundTask)
            backgroundTask = UIBackgroundTaskInvalid
        }
    }
}


回答4:

I think what you are looking for is \"Over the Air Enrollment\" using the Simple Certificate Enrollment Protocol (SCEP). Have a look at the OTA Enrollment Guide and the SCEP Payload section of the Enterprise Deployment Guide.

According to the Device Config Overview you only have four options:

  • Desktop installation via USB
  • Email (attachment)
  • Website (via Safari)
  • Over-the-Air Enrollment and Distribution


回答5:

This page explains how to use images from your bundle in a UIWebView.

Perhaps the same would work for a configuration profile as well.



回答6:

Have you tried just having the app mail the user the config profile the first time it starts up?

-(IBAction)mailConfigProfile {
     MFMailComposeViewController *email = [[MFMailComposeViewController alloc] init];
     email.mailComposeDelegate = self;

     [email setSubject:@\"My App\'s Configuration Profile\"];

     NSString *filePath = [[NSBundle mainBundle] pathForResource:@\"MyAppConfig\" ofType:@\"mobileconfig\"];  
     NSData *configData = [NSData dataWithContentsOfFile:filePath]; 
     [email addAttachmentData:configData mimeType:@\"application/x-apple-aspen-config\" fileName:@\"MyAppConfig.mobileconfig\"];

     NSString *emailBody = @\"Please tap the attachment to install the configuration profile for My App.\";
     [email setMessageBody:emailBody isHTML:YES];

     [self presentModalViewController:email animated:YES];
     [email release];
}

I made it an IBAction in case you want to tie it to a button so the user can re-send it to themselves at any time. Note that I may not have the correct MIME type in the example above, you should verify that.



回答7:

I\'ve though of another way in which it might work (unfortunately I don\'t have a configuration profile to test out with):

// Create a UIViewController which contains a UIWebView
- (void)viewDidLoad {
    [super viewDidLoad];
    // Tells the webView to load the config profile
    [self.webView loadRequest:[NSURLRequest requestWithURL:self.cpUrl]];
}

// Then in your code when you see that the profile hasn\'t been installed:
ConfigProfileViewController *cpVC = 
        [[ConfigProfileViewController alloc] initWithNibName:@\"MobileConfigView\"
                                                      bundle:nil];
NSString *cpPath = [[NSBundle mainBundle] pathForResource:@\"configProfileName\"
                                                   ofType:@\".mobileconfig\"];
cpVC.cpURL = [NSURL URLWithString:cpPath];
// Then if your app has a nav controller you can just push the view 
// on and it will load your mobile config (which should install it).
[self.navigationController pushViewController:controller animated:YES];
[cpVC release];


回答8:

Not sure why you need a configuration profile, but you can try to hack with this delegate from the UIWebView:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
    if (navigationType == UIWebViewNavigationTypeLinkClicked) {
        //do something with link clicked
        return NO;
    }
    return YES;
}

Otherwise, you may consider enable the installation from a secure server.



回答9:

Just host the file on a website with the extension *.mobileconfig and set the MIME type to application/x-apple-aspen-config. The user will be prompted, but if they accept the profile should be installed.

You cannot install these profiles programmatically.



回答10:

This is a great thread, and especially the blog mentioned above.

For those doing Xamarin, here\'s my added 2 cents. I embedded the leaf cert in my app as Content, then used the following code to check it:

        using Foundation;
        using Security;

        NSData data = NSData.FromFile(\"Leaf.cer\");
        SecCertificate cert = new SecCertificate(data);
        SecPolicy policy = SecPolicy.CreateBasicX509Policy();
        SecTrust trust = new SecTrust(cert, policy);
        SecTrustResult result = trust.Evaluate();
        return SecTrustResult.Unspecified == result; // true if installed

(Man, I love how clean that code is, vs. either of Apple\'s languages)