代码之家  ›  专栏  ›  技术社区  ›  Seva Alekseyev

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

  •  69
  • Seva Alekseyev  · 技术社区  · 14 年前

    我想在我的iphone应用程序中附带一个配置文件,并在需要时安装它。

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

    首先,这样的任务是可能的。如果您在网页上放置一个配置配置文件并从safari中单击它,它将被安装。如果您通过电子邮件发送配置文件并单击附件,它也将安装。“installed在本例中意味着installation ui被调用,但我甚至没能做到这一点。

    所以我的理论是启动配置文件安装需要以url的形式导航到它。我已将配置文件添加到我的应用程序包中。

    a) 首先,我在我的bundle中尝试了[sharedapp openurl]的文件://url。没有这样的运气-什么都没发生。

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

    c) 然后我尝试从safari中的bundle加载相同的web页面(使用[sharedapp openurl]——什么都没有发生。我想,Safari看不到我的应用包中的文件。

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

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

    小问题是:什么可以使链接在uiwebview中不可点击?是否可以从 我的 旅行包?如果没有,在iphone上是否有一个本地位置可以放置文件,而safari可以找到它们?

    B编辑: 问题在于我们链接到了一个配置文件。我将它从.mobileconfig重命名为.xml('因为它实际上是xml),更改了链接。链接在我的uiwebview中起作用。重新命名-同样的东西。看起来uiwebview不愿意做应用程序范围的事情——因为配置文件的安装关闭了应用程序。我试着用uiwebviewdelegate告诉它没问题,但这并不能让人信服。uiwebview中mailto:url的行为相同。

    对于邮递: url常见的技术是将它们转换成[openurl]调用,但这对于我的案例来说并不太有效,请参见场景a。

    但是,对于itms:url,uiwebview可以按预期工作……

    编辑2: 试图通过[openurl]向safari提供数据url时无效,请参见此处: iPhone Open DATA: Url In Safari

    EdTe3: 找到了很多关于Safari不支持file://url的信息。然而,uiwebview做了很多事情。另外,在模拟器上的Safari可以很好地打开它们。后一点是最令人沮丧的。


    编辑4: 我从来没有找到解决办法。相反,我构建了一个两位的web界面,用户可以在其中订购发送给他们的配置文件。

    10 回复  |  直到 8 年前
        1
  •  36
  •   Michaël    12 年前

    1)安装本地服务器 RoutingHTTPServer

    2)配置自定义头:

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

    3)为mobileconfig文件(文档)配置本地根路径:

    [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)在控制器中,使用存储在文档中的mobileconfig的名称调用safari:

    [[UIApplication sharedApplication] openURL:[NSURL URLWithString: @"http://localhost:12345/MyProfile.mobileconfig"]];
    
        2
  •  27
  •   xaphod    11 年前

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

    我花了4个小时,但这里有一个解决方案,建立在Malinois拥有一个本地HTTP服务器的想法上:将HTML返回给Safari,Safari会自我刷新;服务器第一次返回MobileConfig,第二次返回自定义URL方案以返回应用程序。用户体验是我想要的:应用程序调用safari,safari打开mobileconfig,当用户在mobileconfig上点击“完成”时,safari再次加载你的应用程序(自定义url方案)。

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

    …下面是从应用程序(即viewcontroller)调用此函数的代码:

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

    希望这能帮助别人。

        3
  •  6
  •   freshking    9 年前

    我已经编写了一个类,用于通过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
            }
        }
    }
    
        4
  •  4
  •   slf    14 年前

    我认为您需要的是使用简单证书注册协议(SCEP)的“空中注册”。看看 OTA Enrollment Guide 以及 Enterprise Deployment Guide .

    根据 Device Config Overview 你只有四个选择:

    • 通过USB安装桌面
    • 电子邮件(附件)
    • 网站(通过Safari)
    • 空中报名和分配
        5
  •  0
  •   Jon-Eric    14 年前

    This page 解释如何在uiwebview中使用包中的图像。

    也许同样的方法也适用于配置概要文件。

        6
  •  0
  •   smountcastle    14 年前

    你有没有试过让应用程序在第一次启动时就把配置文件发给用户?

    -(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类型,您应该验证一下。

        7
  •  0
  •   smountcastle    14 年前

    我想到了另一种可能的工作方式(不幸的是,我没有一个配置文件来测试):

    // 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
  •  0
  •   Hoang Pham    14 年前

    不确定为什么需要配置配置文件,但可以尝试从uiwebview中破解此委托:

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

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

        9
  •  0
  •   Brandon    13 年前

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

    不能以编程方式安装这些配置文件。

        10
  •  0
  •   Eliot Gillum    8 年前

    这是一条很好的线索,尤其是博客 mentioned above .

    对于那些做香豆素的人,这是我加的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
    

    (伙计,我喜欢这段代码有多干净,而不是苹果的任何一种语言)