【问题标题】:Installing a configuration profile on iPhone - programmatically在 iPhone 上安装配置文件 - 以编程方式
【发布时间】:2011-01-21 05:39:46
【问题描述】:

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

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

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

所以我的工作理论是,启动配置文件安装涉及将其作为 URL 导航。我将配置文件添加到我的应用程序包中。

A) 首先,我尝试将 [sharedApp openURL] 与 file:// URL 放入我的包中。没有这样的运气 - 什么也没有发生。

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

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

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

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

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

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

对于 mailto: URL,常见的技术是将它们转换为 [openURL] 调用,但这对我的情况不太适用,请参阅场景 A。

但对于 itms: URL,UIWebView 可以正常工作...

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

EDIT3: 发现很多关于 Safari 不支持 file:// URL 的信息。然而,UIWebView 非常有用。此外,模拟器上的 Safari 可以很好地打开它们。后者是最令人沮丧的。


EDIT4:我从未找到解决方案。相反,我将一个两位 Web 界面放在一起,用户可以在其中订购通过电子邮件发送给他们的个人资料。

【问题讨论】:

  • 这里可能存在安全问题。 Apple 可能不希望您能够从应用程序中更改手机运营商配置文件,这可能会启用网络共享、禁用语音邮件等。
  • 无论如何都需要大量明确的用户协议。此外,我可以直接将用户从我的应用程序引导到相关网页(选项 D),因此他们的控件并不是无懈可击的。
  • Safari 和 Mail 比您的应用程序拥有更多权限。
  • HI Seva,我也想做同样的事情,你终于找到解决方案了吗??
  • @Iphone_bharat:不,不是真的。我做了一个解决方法。

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


【解决方案1】:

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

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

【讨论】:

  • 不。有趣的是,不知何故,这是个人资料的细节。当我提供带有指向它的链接的文本文件时,UIWebView 会按预期导航到它。
【解决方案2】:

您是否尝试过让应用在首次启动时向用户发送配置文件?

-(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]; [电子邮件 addAttachmentData:configData mimeType:@"application/x-apple-aspen-config" fileName:@"MyAppConfig.mobileconfig"]; NSString *emailBody = @"请点击附件为我的应用安装配置文件。"; [电子邮件 setMessageBody:emailBody isHTML:YES]; [自我presentModalViewController:电子邮件动画:是]; [电子邮件发布]; }

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

【讨论】:

  • 会试一试。我不确定邮件撰写过程中的电子邮件附件是否可以打开。此外,指导用户会很痛苦。上面写着“绝望的解决方法”......
  • 用户在撰写时不会打开附件。工作流程将启动您的应用程序,它意识到未安装配置文件,它执行上述操作以启动邮件撰写,用户输入他们的电子邮件地址并点击发送。然后他们打开邮件应用程序并下载电子邮件,单击附件进行安装。我同意这似乎是一个绝望的解决方法。或者,您可以弄清楚 Mail 如何分派 application/x-apple-aspen-config 文件并执行此操作(尽管它可能是一个私有 API,我不知道)。
【解决方案3】:

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

// 创建一个包含 UIWebView 的 UIViewController - (void)viewDidLoad { [超级视图DidLoad]; // 告诉 webView 加载配置文件 [self.webView loadRequest:[NSURLRequest requestWithURL:self.cpUrl]]; } // 然后在您的代码中,当您看到配置文件尚未安装时: ConfigProfileViewController *cpVC = [[ConfigProfileViewController alloc] initWithNibName:@"MobileConfigView" 捆绑:无]; NSString *cpPath = [[NSBundle mainBundle] pathForResource:@"configProfileName" ofType:@".mobileconfig"]; cpVC.cpURL = [NSURL URLWithString:cpPath]; // 如果你的应用有导航控制器,你可以直接推送视图 // on,它将加载您的移动配置(应该安装它)。 [self.navigationController pushViewController:控制器动画:YES]; [cpVC发布];

【讨论】:

  • 这是对选项 B 的轻微改写 - 而不是 HTML,然后链接到配置文件,直接链接到配置文件。我想我已经尝试过了,但没有成功。
【解决方案4】:

我认为您正在寻找的是使用简单证书注册协议 (SCEP) 的“无线注册”。查看OTA Enrollment GuideEnterprise Deployment Guide 的SCEP Payload 部分。

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

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

【讨论】:

  • 看起来像我的选项 D 的一个更高级的版本 :) 不过很好找。
  • 市面上有很多产品可以为您做到这一点,无需自己动手
【解决方案5】:

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

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

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

【讨论】:

  • 这个问题还没有答案吗?
【解决方案6】:

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

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

【讨论】:

    【解决方案7】:

    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) 为了让网络服务器有时间发送文件,请添加:

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

    【讨论】:

    • @SevaAlekseyev 这个解决方案对您有帮助吗?
    • 不,我们重新考虑了整个架构。现在有一个公共反向代理。
    • 您在 AppStore 目标应用程序上使用过这个吗? .mobileconfig 文件是否已使用受信任证书或自签名证书签名?我想知道苹果是否可以拒绝安装自签名移动配置的应用
    • 安装配置文件后是否可以从 Safari 浏览器重定向回应用程序?
    • @igenio SmartJoin.us 已被 App Store 接受,并使用未签名的配置文件从应用程序中的 Web 服务器提供给 Safari
    【解决方案8】:

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

    我花了 4 个小时,但这是基于 malinois 拥有本地 http 服务器的想法的解决方案:您将 HTML 返回到 safari,它会自行刷新;服务器第一次返回 mobileconfig,第二次返回自定义 url-scheme 以返回您的应用程序。 UX 是我想要的:应用程序调用 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"];
        }
    }
    

    ...这是从应用程序(即视图控制器)调用它的代码:

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

    希望这对某人有所帮助。

    【讨论】:

    • 如果这行得通,那就太棒了。非常感谢。我找了很久的解决方案。
    • 我使用成功了。您可能需要调整重新加载时间间隔——此处为 400 毫秒
    • 安装后还有其他方法可以返回应用吗?用户正在按下“完成”按钮,我想知道是否有任何处理程序?
    • 如果您指的是用于安装配置文件的本机 UI,以及右上角的“完成”按钮,那么不,没有公共 API 可以控制我所知道的内容。
    • 这太棒了。会试试这个。绝对是企业应用程序的巨大胜利。
    【解决方案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
            }
        }
    }
    

    【讨论】:

    • 也为我工作!请注意,此处显示的代码仅适用于旧版本的 Swifter。
    • 我已将代码转换为 Swift 3 和最新的 Swifter。工作得很好,但是当系统返回 Safari 时遇到困难,Safari 重新加载页面(并进入循环)并且返回应用程序的对话框消失(并且需要终止 Safari)。谢谢
    • 我的 Swift 3 附加编辑被一些对 ios 和/或 Swift(以及版本 2 和 3 之间的区别)一无所知的聪明人拒绝。我把它放在了要点中,而不是gist.github.com/3ph/beb43b4389bd627a271b1476a7622cc5。我知道发布链接不符合 SO,但显然有些人也是如此。
    • 你的要点真的很棒,曾经为我工作过。之后,我必须重置 safari 缓存以使其再次工作。我对附加元信息和标题字段的测试并没有解决它。我找到的唯一解决方案是在 setupHandlers() 中随机化“安装”-String。
    • 好的,我解决了我的问题。它仅适用于方案。将空字符串作为返回 url 会导致所描述的行为。使用有效的方案,将显示对话框,但之后 safari 被破坏(取消对话框时)。而不是返回 .MovedPermanently(self.returnURL) 我建议返回一个带有按钮的网页。在错误情况下,用户可以关闭页面。
    【解决方案10】:

    这是一个很棒的帖子,尤其是博客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 的任何一种语言相比,我喜欢这段代码的简洁程度)

    【讨论】:

    • 您使用的是私有 API 吗?还是 Xamarin 开发人员干了脏活?
    • 配置文件完成繁重的工作,不需要私有 API。肮脏的是引导用户从应用程序安装配置文件的过程——iOS 只能处理来自 Safari 或 Mail 的文件,这并不是最流畅的体验,之后让用户回到应用程序是额外的乐趣。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-03-04
    • 2011-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多