Установка профиля конфигурации на iPhone - программно
Я хотел бы отправить профиль конфигурации с моим iPhone-приложением и установить его, если необходимо.
Помните, мы говорим о профиле конфигурации, а не профиле обеспечения.
Во-первых, такая задача возможна. Если вы разместите профиль конфигурации на веб-странице и щелкните по нему из Safari, он будет установлен. Если вы отправляете по электронной почте профиль и нажимаете вложение, оно также будет установлено. "Установленный" в этом случае означает "Инициализируется пользовательский интерфейс установки" - но я даже не мог дойти до этого.
Итак, я работал над теорией о том, что инициация установки профиля включает в себя навигацию к ней в качестве URL-адреса. Я добавил профиль в свой пакет приложений.
A) Сначала я попробовал [sharedApp openURL] с файлом://URL в свой пакет. Нет такой удачи - ничего не происходит.
B) Затем я добавил HTML-страницу в свой пакет, который имеет ссылку на профиль и загрузил его в UIWebView. Нажатие на ссылку ничего не делает. Однако загрузка идентичной страницы с веб-сервера в Safari работает отлично - ссылка доступна для клика, профиль устанавливается. Я предоставил UIWebViewDelegate, отвечая YES на каждый запрос навигации - без разницы.
C) Затем я попытался загрузить одну и ту же веб-страницу из своего пакета в Safari (используя [sharedApp openURL] - ничего не происходит. Я думаю, Safari не может видеть файлы внутри моего пакета приложений.
D) Загрузка страницы и профиля на веб-сервере выполнима, но боль на организационном уровне, не говоря уже о дополнительном источнике сбоев (что, если нет покрытия 3G?).
Итак, мой большой вопрос: ** как программно установить профиль?
И маленькие вопросы: что может сделать ссылку без кликов в UIWebView? Можно ли загрузить файл://URL из моего пакета в Safari? Если нет, есть ли локальное местоположение на iPhone, где я могу разместить файлы, и Safari может их найти?
EDIT on B): проблема заключается в том, что мы привязываемся к профилю. Я переименовал его из .mobileconfig в .xml(потому что это действительно XML), изменил ссылку. И ссылка работала в моем UIWebView. Переименовал его обратно - то же самое. Похоже, что UIWebView неохотно выполняет все приложения - поскольку установка профиля закрывает приложение. Я пытался сказать, что все в порядке - с помощью UIWebViewDelegate - но это не убеждало. Такое поведение для mailto: URL-адресов в интерфейсе UIWebView.
Для URL-адресов mailto: общий способ заключается в том, чтобы перевести их в вызовы [openURL], но это не совсем работает для моего случая, см. сценарий A.
Для itms: URL, однако UIWebView работает как ожидалось...
EDIT2: пытался подавать URL-адрес данных в Safari через [openURL] - не работает, см. здесь: iPhone Open DATA: Url В Safari
EDIT3: нашел много информации о том, как Safari не поддерживает файлы://URL. UIWebView, однако, очень нравится. Кроме того, Safari на симуляторе открывает их просто отлично. Последний бит наиболее расстраивает.
EDIT4: Я не нашел решения. Вместо этого я собрал двухбитовый веб-интерфейс, где пользователи могут заказать профиль, отправленный им по электронной почте.
Ответы
Ответ 1
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) В вашем контроллере вызовите сафари с именем файла mobileconfig, хранящегося в документах:
[[UIApplication sharedApplication] openURL:[NSURL URLWithString: @"http://localhost:12345/MyProfile.mobileconfig"]];
Ответ 2
Ответ от malinois работал у меня, НО, я хотел, чтобы решение, вернувшееся в приложение, автоматически после того, как пользователь установил mobileconfig.
Мне потребовалось 4 часа, но вот решение, основанное на идее malinois о наличии локального http-сервера: вы возвращаете HTML в сафари, обновляющее себя; в первый раз, когда сервер возвращает файл mobileconfig, а во второй раз возвращает пользовательскую схему url, чтобы вернуться в ваше приложение. UX - это то, что я хотел: приложение вызывает сафари, сафари открывает mobileconfig, когда пользователь нажимает "done" на mobileconfig, затем сафари снова загружают ваше приложение (настраиваемая схема 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
Я написал класс для установки файла mobileconfig через Safari и затем возвращаюсь в приложение. Он полагается на сервер 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
Я думаю, что то, что вы ищете, это "Over the Air Enrollment", используя протокол Simple Certificate Enrollment Protocol (SCEP). Посмотрите Руководство по регистрации OTA и раздел полезной нагрузки SCEP Руководство по развертыванию предприятия.
В соответствии с Обзор конфигурации устройства у вас есть только четыре варианта:
- Установка рабочего стола через USB
- Электронная почта (вложение)
- Веб-сайт (через Safari)
- Зачисление и распределение по воздуху
Ответ 5
Эта страница объясняет, как использовать изображения из вашего пакета в UIWebView.
Возможно, то же самое будет работать и для профиля конфигурации.
Ответ 6
Пробовал ли вы просто использовать приложение для отправки пользователю профиля конфигурации при первом запуске?
-(IBAction)mailConfigProfile {
MFMailComposeViewController *email = [[MFMailComposeViewController alloc] init];
email.mailComposeDelegate = self;
[email setSubject:@"My App 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
У меня есть другой способ, которым он может работать (к сожалению, у меня нет профиля конфигурации для тестирования):
// 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
Не уверен, зачем нужен профиль конфигурации, но вы можете попытаться взломать этот делегат из UIWebView:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
if (navigationType == UIWebViewNavigationTypeLinkClicked) {
//do something with link clicked
return NO;
}
return YES;
}
В противном случае вы можете включить установку с безопасного сервера.
Ответ 9
Просто разместите файл на веб-сайте с расширением *.mobileconfig и установите тип MIME в application/x-apple-aspen-config. Пользователь будет запрошен, но если они согласятся, профиль должен быть установлен.
Вы не можете программно установить эти профили.
Ответ 10
Это отличный поток, и особенно блог упомянутый выше.
Для тех, кто делает Xamarin, здесь мои добавленные 2 цента. Я включил сертификат листа в свое приложение как Content, а затем использовал следующий код, чтобы проверить его:
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)