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


74

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

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

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

因此,我根据以下理论进行工作:启动配置文件安装涉及以URL导航到它。我已将个人资料添加到我的应用程序包中。

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

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

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

D)将页面和配置文件上传到Web服务器上是可行的,但是在组织层面上却是​​一个痛苦,更不用说额外的失败原因了(如果没有3G覆盖,该怎么办?

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

小问题是:什么可以使UIWebView中的链接不可单击?是否有可能加载一个文件:从// URL在Safari浏览器捆绑?如果没有,iPhone上是否可以放置文件,而Safari可以找到它们?

在B)上编辑:问题在于我们正在链接到配置文件。我将其从.mobileconfig重命名为.xml(因为它确实是XML),更改了链接。链接在我的UIWebView中有效。重新命名-同样的东西。似乎UIWebView不愿做应用程序范围的事情-因为配置文件的安装会关闭应用程序。我试图通过UIWebViewDelegate告诉它可以,但是并没有说服。mailto的行为相同:UIWebView中的URL。

对于mailto: URL,常见的技术是将它们转换为[openURL]调用,但是对于我的情况而言,这并不起作用,请参阅方案A。

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

EDIT2:尝试通过[openURL]向Safari提供数据URL-不起作用,请参见此处:iPhone打开数据:Safari中的网址

EDIT3:找到了很多有关Safari如何不支持file:// URL的信息。但是,UIWebView确实可以做到。另外,在模拟器上打开Safari也可以。后一点是最令人沮丧的。


EDIT4:我从未找到解决方案。取而代之的是,我整理了一个两位的Web界面,用户可以在其中订购通过电子邮件发送给他们的个人资料。


2
这里可能存在安全隐患。Apple可能不希望您能够从应用程序中更改手机运营商配置文件,这可能会启用网络共享,禁用语音邮件等
布拉德·拉尔森

2
无论如何,都需要大量的明确的用户协议。此外,我可以直接从我的应用程序(选项D)将用户定向到相关的网页,所以这并不意味着他们的控件是不透气的。
塞瓦·阿列克谢耶夫

Safari和Mail具有比您的应用程序更多的特权。
2010年

嗨,塞瓦,我也想做同样的事情,你终于找到解决办法了吗?
Iphone_bharat'3

@Iphone_bharat:不,不是真的。我做了一个解决方法。
塞瓦(Seva Alekseyev)

Answers:


38

1)安装诸如RoutingHTTPServer之类的本地服务器

2)配置自定义标头:

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

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

[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此解决方案对您有帮助吗?
奥列格·达努

不,我们重新考虑了整个体系结构。现在有一个公共反向代理。
塞瓦·阿列克谢耶夫

2
您是否在AppStore指定的应用程序上使用了此功能?.mobileconfig文件是否已使用受信任的证书或具有自签名证书的文件进行签名?我想知道苹果是否可以拒绝安装自签名
mobileconfig

2
安装配置文件后,是否可以从野生动物园浏览器重定向回到应用程序?
Abilash Balasubramanian

2
@igenio SmartJoin.us被接受进入App Store,并使用它从一个Web服务器的应用,最多可服务Safari的一个无符号的配置文件
alfwatt

28

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

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

...这是从应用程序(即viewcontroller)调用此代码的代码:

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

希望这对某人有帮助。


1
如果可行,那就太棒了。非常感谢。我一直在寻找解决方案。
2014年

我正在成功使用它。您可能需要调整重新加载时间间隔-此处为400ms
xaphod 2014年

信息化之后,还有其他方法可以返回到应用程序吗?用户按下“完成”按钮,我想知道是否有任何处理程序?
EmilDo

如果您指的是用于安装配置文件的本机UI,并且位于右上角是“完成”按钮,那么没有,没有公共API可以控制我所知道的内容。
xaphod 2014年

1
这真太了不起了。将尝试一下。绝对是企业应用程序的巨大胜利。
ninjaneer 2015年

8

我编写了一个用于通过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。
Pieter Meiresone

1
我已经将代码转换为Swift 3和最新的Swifter。效果很好,但是当系统返回Safari时遇到困难,Safari会重新加载页面(并进入循环),并且返回到应用程序的对话框会消失(并且Safari需要终止)。谢谢
Tom

4
我的Swift 3附加编辑被一些对ios和/或Swift不了解(以及版本2和3之间的区别)的聪明人拒绝了。我将其放在gist中,而不是gist.github.com/3ph/beb43b4389bd627a271b1476a7622cc5中。我知道发布链接与SO背道而驰,但显然有些人也是如此。
汤姆(Tom)

1
您的要旨真的很棒,曾经为我工作过。之后,我必须重置野生动物园缓存才能使其再次正常工作。我对其他元信息和标头字段的测试无法解决。我发现的唯一解决方案是将setupHandlers()中的“ install” -String随机化。
ObjectAlchemist

2
好的,我解决了我的问题。它仅适用于方案。给出一个空字符串作为返回URL会导致所描述的行为。使用有效的方案时,将显示对话框,但是之后,Safari将被破坏(取消对话框时)。我建议不要返回带有内部按钮的网页,而不是返回.MovedPermanently(self.returnURL)。在错误的情况下,用户能够关闭该页面。
ObjectAlchemist


1

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

-(IBAction)mailConfigProfile {
     MFMailComposeViewController * email = [[MFMailComposeViewController alloc] init];
     email.mailComposeDelegate =自我;

     [发送电子邮件给setSubject:@“我的应用程序的配置文件”];

     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 = @“请点击附件以安装“我的应用”的配置文件。
     [email setMessageBody:emailBody isHTML:YES];

     [自身presentModalViewController:email动画:是];
     [电子邮件发布];
}

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


将尝试一下。我不确定邮件撰写过程中的电子邮件附件是否可打开。此外,指导用户会很痛苦。上面写满了“绝望的解决方法” ...
Seva Alekseyev

用户在撰写时不会打开附件。工作流程将启动您的应用程序,它意识到未安装配置文件,它执行上述操作以启动邮件撰写,用户键入其电子邮件地址并发送点击。然后,他们打开邮件应用程序并下载电子邮件,单击附件进行安装。我同意这似乎是一种绝望的解决方法。另外,您可以弄清楚Mail如何调度应用程序/ x-apple-aspen-config文件并执行此操作(尽管它可能是私有API,但我不知道)。
smountcastle,2010年

1

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

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


0

这一页说明如何在UIWebView中使用捆绑包中的图像。

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


1
不。有趣的是,它以某种方式描述了配置文件的细节。当我提供带有链接的文本文件时,UIWebView将按预期导航到该文件。
塞瓦·阿列克谢耶夫

0

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

//创建一个包含UIWebView的UIViewController
-(void)viewDidLoad {
    [super viewDidLoad];
    //告诉webView加载配置文件
    [self.webView loadRequest:[NSURLRequest requestWithURL:self.cpUrl]];
}

//然后在代码中看到尚未安装配置文件时:
ConfigProfileViewController * cpVC = 
        [[ConfigProfileViewController分配] initWithNibName:@“ MobileConfigView”
                                                      bundle:nil];
NSString * cpPath = [[NSBundle mainBundle] pathForResource:@“ configProfileName”
                                                   ofType:@“。mobileconfig”];
cpVC.cpURL = [NSURL URLWithString:cpPath];
//然后,如果您的应用具有导航控制器,则只需按一下视图即可 
//开启,它将加载您的移动配置(应该安装它)。
[self.navigationController pushViewController:controller动画:是];
[cpVC版本];

这是选项B的稍微改写-代替HTML,然后链接到配置文件,立即链接到配置文件。我想我一路尝试都没有成功。
塞瓦·阿列克谢耶夫

0

不确定为什么需要配置文件,但是可以尝试通过UIWebView对此委托进行破解:

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

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


这个问题仍然没有答案吗?
Hoang Pham

0

这是一个很棒的话题,尤其是上面提到的博客。

对于那些使用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开发人员做了肮脏的工作?
Ohad Cohen

配置配置文件可以完成繁重的工作,不需要专用API。肮脏的是引导用户通过应用程序安装配置文件的过程-iOS只能处理Safari或Mail中的文件,这并不是最顺畅的体验,让用户随后重新回到应用程序中非常有趣。
艾略特·吉伦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.