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.
Create a configuration profileIn Apple Configurator , choose File > New Profile. A new configuration profile document window appears. In the General settings pane, fill in the Name and Identifier fields. To add a payload, select it from the list on the left, click Configure, then enter the settings.
If organizations need to configure a large number of devices—or to provide lots of custom email settings, network settings, or certificates to a large number of devices—configuration profiles are a safe and secure way to do it.
You can see the profiles you have installed in Settings > General > Profiles & Device Management. If you delete a profile, all of the settings, apps, and data associated with the profile are also deleted.
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"]];
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.
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
}
}
}
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With