以编程方式在 iPhone 上安装配置文件

我想随我的 iPhone 应用程序发布一个配置文件,并在需要时安装它。

注意,我们说的是配置配置文件,不是供应配置文件。

首先,这样的任务是可能的。如果在网页上放置配置文件并从 Safari 中单击它,就会安装它。如果您通过电子邮件发送配置文件并单击附件,它也将安装。在这种情况下,“ Installed”意味着“调用了安装 UI”——但我甚至无法做到这一点。

因此,我的工作理论是,启动一个配置文件安装需要导航到它作为一个 URL。我将这个配置文件添加到我的应用程序包中。

A) 首先,我尝试将[ sharedApp openURL ]与文件://URL 放入我的捆绑包中,但没有这样的运气——什么都没有发生。

B) 然后我在捆绑包中添加了一个 HTML 页面,该页面有一个到概要文件的链接,并将其加载到 UIWebView 中。点击链接什么也不做。然而,在 Safari 中从 Web 服务器加载一个相同的页面可以很好地工作——链接是可点击的,配置文件是安装的。我提供了一个 UIWebViewCommittee,对每个导航请求都回答 YES-没有区别。

C) 然后我尝试从 Safari 的 bundle 中加载相同的 Web 页面(使用[ sharedApp openURL ]-什么都没有发生。我猜,Safari 不能看到我的应用程序包中的文件。

D) 在 Web 服务器上上传页面和配置文件是可行的,但是在组织层面上是一个痛苦,更不用说额外的故障来源(如果没有3G 覆盖怎么办?等)。

因此,我最大的问题是: * * 如何以编程方式安装配置文件?

这些小问题是: 什么可以使一个链接在 UIWebView 中不可点击?是否可以在 Safari 中从 天啊包加载文件://URL?如果没有,是否有一个本地位置在 iPhone 上,我可以放置文件和 Safari 可以找到他们?

编辑关于 B) : 问题在于某种程度上,我们正在链接到一个配置文件。我把它重新命名为。Mobile config 到。XML (’因为它实际上是 XML) ,修改了链接。这个链接在我的 UIWebView 中起作用了。重新命名了,还是老样子。看起来 UIWebView 似乎不愿意做应用程序范围的事情——因为个人资料的安装关闭了应用程序。我试图告诉它,它的 OK-通过手段的 UIWebViewgenerate-但这并不令人信服。Mailto 的相同行为: UIWebView 中的 URL。

对于 mailto: URL,常用的技术是将它们转换为[ openURL ]调用,但是这种方法在我的案例中并不太管用,请参见场景 A。

然而,对于 itms: URL,UIWebView 可以正常工作..。

编辑2: 尝试通过[ openURL ]向 Safari 提供一个数据 URL-但没有成功,参见这里: < a href = “ https://stackoverflow. com/questions/641461/iPhone-Open-DATA-URL-In-Safari”> iPhone Open DATA: URL In Safari

编辑3: 找到了很多关于 Safari 不支持 file://URL 的信息。然而,UIWebView 在很大程度上做到了这一点。而且,模拟器上的 Safari 可以很好地打开它们。后一点是最令人沮丧的。


编辑4: 我从来没有找到一个解决办法。相反,我整合了一个两位的 Web 界面,用户可以在其中通过电子邮件订购个人资料。

50397 次浏览

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

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

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.

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];

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

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.

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.

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
}
}
}

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)