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

时间:2010-02-25 22:07:20

标签: iphone cocoa-touch ios-simulator configuration-files

我想使用我的iPhone应用程序发送配置文件,并在需要时安装它。

请注意,我们讨论的是配置文件,而不是配置文件。

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

所以我的理论是,启动配置文件安装涉及以URL的形式导航到它。我将个人资料添加到了我的应用包中。

A)首先,我尝试将[sharedApp openURL]与file:// URL一起放入我的包中。没有这样的运气 - 没有任何反应。

B)然后我在我的包中添加了一个HTML页面,其中包含指向该配置文件的链接,并将其加载到UIWebView中。点击链接什么都不做。但是,从Safari中的Web服务器加载相同的页面可以正常工作 - 链接是可点击的,配置文件会安装。我提供了一个UIWebViewDelegate,对每个导航请求都回答“是” - 没有区别。

C)然后我尝试从Safari中的捆绑包加载相同的网页(使用[sharedApp openURL] - 没有任何反应。我猜,Safari无法在我的应用包中看到文件。

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

所以我的重要问题是:**如何以编程方式安装配置文件?

小问题是:什么可以使UIWebView中的链接不可点击?是否可以从Safari中的 my 包中加载文件:// URL?如果没有,iPhone上有本地位置我可以放置文件吗?Safari可以找到它们吗?

在B上编辑:问题在某种程度上我们正在链接到个人资料。我将它从.mobileconfig重命名为.xml(因为它真的是XML),改变了链接。这个链接在我的UIWebView中工作。重新命名 - 同样的东西。看起来UIWebView似乎不愿意做应用程序范围的事情 - 因为安装配置文件会关闭应用程序。我尝试通过UIWebViewDelegate告诉它没关系 - 但这并没有让人信服。 mailto:UIWebView中的URL的行为相同。

对于mailto:网址,常见的技巧是将它们转换为[openURL]调用,但这对我的情况并不适用,请参阅方案A.

对于itms:但是,UIWebView按预期工作...

EDIT2:尝试通过[openURL]向Safari提供数据网址 - 不起作用,请参阅此处:iPhone Open DATA: Url In Safari

EDIT3:发现了很多关于Safari如何不支持file://网址的信息。然而,UIWebView非常有用。此外,模拟器上的Safari打开它们就好了。后一点是最令人沮丧的。


EDIT4:我从未找到解决方案。相反,我整理了一个两位的Web界面,用户可以通过电子邮件订购配置文件。

10 个答案:

答案 0 :(得分:36)

1)安装本地服务器,如RoutingHTTPServer

2)配置自定义标题:

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

3)配置mobileconfig文件的本地根路径(Documents):

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

4)为了让Web服务器有时间发送文件,请添加:

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)在您的控制器中,使用存储在Documents:

中的mobileconfig名称调用safari
[[UIApplication sharedApplication] openURL:[NSURL URLWithString: @"http://localhost:12345/MyProfile.mobileconfig"]];

答案 1 :(得分:27)

malinois的答案对我有用,但是,我想要一个在用户安装mobileconfig后自动返回应用程序的解决方案。

我花了4个小时,但这里是解决方案,建立在malinois的想法,即拥有一个本地的http服务器:你将HTML返回到自我更新的safari;第一次服务器返回mobileconfig,第二次返回自定义url-scheme以返回到您的应用程序。用户体验是我想要的:应用程序调用safari,safari打开mobileconfig,当用户在mobileconfig上点击“完成”,然后safari再次加载你的应用程序(自定义网址方案)。

- (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"];
    }
}

...这里是从app(即viewcontroller)调用此代码的代码:

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

希望这有助于某人。

答案 2 :(得分:6)

我编写了一个类,用于通过Safari安装mobileconfig文件,然后返回到应用程序。它依赖于我发现运行良好的http服务器引擎Swifter。 我想在下面分享我的代码来做这件事。它的灵感来自于我在www中发现的多个代码源。因此,如果您找到自己的代码,贡献给您。

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

答案 3 :(得分:4)

我认为您正在寻找的是使用简单证书注册协议(SCEP)的“空中注册”。请查看OTA Enrollment GuideEnterprise Deployment Guide和SCEP有效负载部分。

根据Device Config Overview,您只有四个选项:

  • 通过USB进行桌面安装
  • 电子邮件(附件)
  • 网站(通过Safari)
  • 无线注册和分发

答案 4 :(得分:0)

This page解释了如何在UIWebView中使用捆绑包中的图像。

也许同样适用于配置文件。

答案 5 :(得分:0)

您是否曾尝试让应用程序在第一次启动时向用户发送配置文件?

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

我将它设为IBAction,以防您想将其绑定到一个按钮,以便用户可以随时将其重新发送给自己。请注意,我在上面的示例中可能没有正确的MIME类型,您应该验证。

答案 6 :(得分:0)

我有另一种方式可以使用它(遗憾的是我没有配置文件可以测试):

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

答案 7 :(得分:0)

不确定为什么需要配置文件,但您可以尝试使用UIWebView中的此委托进行攻击:

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

否则,您可以考虑从安全服务器启用安装。

答案 8 :(得分:0)

只需在扩展名为* .mobileconfig的网站上托管该文件,并将MIME类型设置为application / x-apple-aspen-config。将提示用户,但如果他们接受,则应安装该配置文件。

您无法以编程方式安装这些配置文件。

答案 9 :(得分:0)

这是一个很棒的主题,特别是博客mentioned above

对于那些做Xamarin的人来说,这是我增加的2美分。我将叶证书作为内容嵌入到我的应用程序中,然后使用以下代码进行检查:

        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

(伙计,我喜欢这种代码有多干净,与Apple的任何一种语言相比)