Installing a configuration profile on iPhone - programmatically
Asked Answered
R

10

76

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.

Restate answered 25/2, 2010 at 22:7 Comment(9)
There might be security concerns at work here. Apple might not want you to be able to change a cell carrier configuration file from within an application, which could enable tethering, disable voicemail, etc.Trichromat
A good deal of explicit user agreement is required anyway. Besides, I could just direct the user to the relevant web page from my app (option D), so it's not like their controls are airtight.Restate
Safari and Mail have more privileges than your application.Marteena
HI Seva, i also want to do the same , did u finally get a solution??Jugendstil
@Iphone_bharat: no, not really. I did a workaround.Restate
You can add a http server in your app, launch a request in safari in localhost and install it.Billybillycock
@malinois: does that work on jailed phones? Have you tried or are you just assuming?Restate
I have added an answer which works on all devices...Billybillycock
I'm guessing this same restriction applies to TestFlight-style app installation. For the same reasons. Which is why neither TestFlight or HockeyApp have a real native app on iOS.Cornucopia
W
39

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"]];
Winer answered 24/3, 2012 at 19:44 Comment(9)
@SevaAlekseyev Did this solution ever helped you?Unbelievable
No, we rethought the whole architecture instead. There's a public reverse proxy now.Restate
I assume the app "App Icons" does exactly this. You can install configuration including web clip icons, even with Air plane mode turned on. itunes.apple.com/jp/app/ios-7-yongniapuriaikon-homu/…Enjoyment
Can you please explain how to install RoutingHTTPServer? I am not able to build this and install on iPhone.Layton
i got this work in my app, and uploaded it to App store, but Apple rejected it saying "We found that your app uses public APIs in a manner not prescribed by Apple". Does Any one have suggestion? What can i do now?Cochran
It seems the this doesn't work for iOS 9... anyone can confirm?Burson
Did you used this on an AppStore destinated app? Was the .mobileconfig file already signed with a trusted certificate o with a self-signed certificate? I would like to know if apple could reject an app that install a self-signed mobileconfigTorbart
Is it possible to redirect back to the app from safari browser once we install our configure profile?Caylor
@igenio SmartJoin.us was accepted into the App Store and uses an unsigned configuration profile which it serves up to the Safari from a web server in the appBannockburn
C
28

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.

Crosscheck answered 9/1, 2014 at 7:39 Comment(13)
if this works then awesome. Thanks a lot. I was looking for the solution for a long time.Galatia
I am using this successfully. You might need to adjust the reload time interval -- 400ms hereCrosscheck
Is there any other way to return to the app after isntallation? The user is pressing "done" button and I wonder if there is any handler for that?Dulce
If you mean the native UI for installing a profile, and the Done button at the top-right, then no, there's no public API that allows control of that which I know of.Crosscheck
It goes back to app after installation of profile. However, when I open safari again, it always also go to the app. How can I stop to go back to app and only allow one time?Candlemas
Try adding to the javascript to close the window after reload, or, execute the action a set number of times. You will need to experimentCrosscheck
this is amazing. will try this out. definitely a huge win for enterprise apps.Interradial
@Crosscheck Hey I have implemented the code, which you have mentioned above. And I am quite near to get things to be done. My problem is. When I launch the application, first it open up the blank page in safari and then when I come back to application then it redirect me to profile installation page. Any idea why this is happening. Deadly stuck here. Any help is appreciated. Why initially it is opening blank page in safari? Tried multiple things but no luck yet. Please help me outInterpolation
Because you are serving a blank page with javascript. Thats what the answer is.Crosscheck
@Crosscheck Then what change I should make in above code?Interpolation
@Crosscheck You can answer on my question as well if you know the answer. #43337403Interpolation
what about the permissions required in the settings?Spite
@Crosscheck is this possible to make it work in cross-platform mobile techs like Flutter? Do we need to write this in native side only? do you have swift code for this?Hepatic
C
11

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
        }
    }
}
Crescent answered 17/10, 2015 at 8:7 Comment(7)
Worked for me as well! Note that the code shown here only works for an older version of Swifter.Scalage
I've converted the code to Swift 3 and the latest Swifter. Works quite well but having difficulty when the system returns back to Safari, the Safari reloads the page (and gets into loop) and the dialog for going back to the app disappears (and Safari needs to be killed). ThanksHermann
My Swift 3 addition edit was rejected by some smart people who know nothing about ios and/or Swift (and the difference between version 2 and 3). I've put it in gist instead gist.github.com/3ph/beb43b4389bd627a271b1476a7622cc5. I know posting links goes against SO but so do apparently some people.Hermann
Your gist is really great and worked for me once. After that I have to reset the safari cache to get it work again. My tests with additional meta informations and header fields didn't solve it. Only solution I found is to randomize the "install"-String in setupHandlers().Frequent
Ok, I solved my problem. It only works with a scheme. Giving an empty string as return url results in the described behaviour. With a valid scheme the dialog will be shown, but after that the safari is broken (when cancelling the dialog). Instead of returning a .MovedPermanently(self.returnURL) I suggest to return a webpage with a button inside. In error case user is able to close the page than.Frequent
how do i use in my project?Elgon
Will it pass App Store review criteria?Taeniacide
S
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
Sassaby answered 25/4, 2010 at 15:12 Comment(2)
Looks like a fancier version of my option D :) Good find, though.Restate
there are tons of products out there that do it for you, no need to roll your ownSassaby
C
1

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.

Cinchonism answered 21/4, 2010 at 15:33 Comment(2)
Will give it a try. I'm not sure that an e-mail attachment in the process of mail composition is openable. Besides, instructing the users will be a pain. Has "desperate workaround" written all over it...Restate
The user wouldn't open the attachment while composing. The work-flow would be launch your app, it realizes that the config profile isn't installed, it does the above to initiate the mail composition, the user types in their email address and hits send. Then they open Mail app and download the email, clicking on the attachment to install it. I agree that it seems like a desperate workaround. Alternatively you can figure out how Mail is dispatching the application/x-apple-aspen-config file and just do that (though it might be a private API, I don't know).Cinchonism
J
1

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.

Jylland answered 14/7, 2011 at 20:30 Comment(0)
M
0

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

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

Marshy answered 25/2, 2010 at 22:48 Comment(1)
Nope. And the funny part is, it's somehow the specifics of the profile. When I provide a text file with a link to it, the UIWebView navigates to it as expected.Restate
C
0

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];
Cinchonism answered 22/4, 2010 at 13:29 Comment(1)
That's a slight rephrasing of option B - instead of HTML, then link to profile, link to the profile straight away. I think I've tried this, unsuccessfully, along the way.Restate
S
0

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.

Splutter answered 3/1, 2011 at 10:47 Comment(1)
Is this question still unanswered?Splutter
V
0

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)

Verisimilitude answered 30/11, 2016 at 5:30 Comment(2)
are you using private API's? or did Xamarin developers do the dirty job?Lole
The configuration profile does the heavy lifting, no private APIs required. What's dirty is the process for walking a user through installing a profile from an app--iOS can only process the file from Safari or Mail, it's not exactly the smoothest of experiences and getting the user back into the app afterwards is extra fun.Verisimilitude

© 2022 - 2024 — McMap. All rights reserved.