Встановлення профілю конфігурації на iPhone - програмно


74

Я хотів би надіслати конфігураційний профіль із моєю програмою iPhone та встановити його, якщо це потрібно.

Майте на увазі, ми говоримо про профіль конфігурації, а не про профіль підготовки.

По-перше, таке завдання можливо. Якщо ви розмістите конфігураційний профіль на веб-сторінці та натиснете його в Safari, він буде встановлений. Якщо ви надішлете профіль електронною поштою та клацнете вкладення, він також встановиться. "Встановлено" в цьому випадку означає "Викликається інтерфейс встановлення", але я навіть не зміг зайти так далеко.

Отже, я працював згідно з теорією, згідно з якою ініціювання встановлення профілю передбачає перехід до нього як URL-адреси. Я додав профіль до свого набору додатків.

A) По-перше, я спробував [sharedApp openURL] з URL-адресою file: // у свій пакет. Немає такої удачі - нічого не відбувається.

Б) Потім я додав HTML-сторінку до свого набору, що має посилання на профіль, і завантажив її в UIWebView. Натискання на посилання нічого не робить. Однак завантаження однакової сторінки з веб-сервера в Safari працює нормально - посилання можна натиснути, профіль встановлюється. Я надав UIWebViewDelegate, відповідаючи ТАК на кожен навігаційний запит - немає різниці.

В) Потім я спробував завантажити ту саму веб-сторінку зі свого набору в Safari (використовуючи [sharedApp openURL] - нічого не відбувається. Я думаю, Safari не може бачити файли в моєму пакеті додатків.

Г) Завантаження сторінки та профілю на веб-сервер здійсненно, але це завдає біль на організаційному рівні, не кажучи вже про додаткове джерело збоїв (що, якщо немає покриття 3G? Тощо).

Тож моє велике запитання: ** як мені встановити профіль програмно?

І маленькі запитання: що може зробити посилання некліковим в UIWebView? Чи можна завантажити URL-адресу file: // з мого набору в Safari? Якщо ні, чи є на iPhone місцезнаходження, де я можу розміщувати файли, і Safari може їх знаходити?

EDIT на B): проблема якось полягає в тому, що ми робимо посилання на профіль. Я перейменував його з .mobileconfig на .xml (тому що це справді XML), змінив посилання. І посилання працювало в моєму UIWebView. Перейменовано назад - те саме. Схоже, UIWebView неохоче робить загальнодоступні речі - оскільки встановлення профілю закриває програму. Я спробував сказати, що це нормально - за допомогою UIWebViewDelegate - але це не переконало. Така ж поведінка для mailto: URL-адреси в UIWebView.

Для mailto: URL-адрес загальним методом є переведення їх у виклики [openURL], але це не зовсім працює для мого випадку, див. Сценарій А.

Для itms: URL-адрес, однак, UIWebView працює належним чином ...

EDIT2: спробував подати URL-адресу даних до Safari за допомогою [openURL] - не працює, див. Тут: iPhone Open DATA: URL- адреса в Safari

EDIT3: знайшов багато інформації про те, як Safari не підтримує файли: // URL-адреси. Однак UIWebView дуже багато чого робить. Крім того, Safari на тренажері відкриває їх просто чудово. Останній біт є найбільш неприємним.


EDIT4: Я так і не знайшов рішення. Натомість я склав двобітний веб-інтерфейс, де користувачі можуть замовити профіль, надісланий їм електронною поштою.


2
Тут можуть бути проблеми з безпекою на роботі. Apple може не хотіти, щоб ви мали змогу змінити файл конфігурації стільникового оператора всередині програми, що могло б увімкнути модем, вимкнути голосову пошту тощо
Бред Ларсон

2
У будь-якому випадку потрібна велика кількість явної угоди користувача. Крім того, я міг би просто спрямувати користувача на відповідну веб-сторінку з мого додатка (варіант D), тому це не схоже на те, що їх елементи управління герметичні.
Сева Алексєєв

Safari та Mail мають більше привілеїв, ніж ваша програма.
розіграш уперед

Привіт Сева, я теж хочу зробити те саме, чи нарешті ти отримав рішення ??
Iphone_bharat

@Iphone_bharat: ні, не дуже. Я зробив обхідний шлях.
Сева Олексієв

Відповіді:


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) У своєму контролері зателефонуйте в сафарі з назвою mobileconfig, що зберігається в Документах:

[[UIApplication sharedApplication] openURL:[NSURL URLWithString: @"http://localhost:12345/MyProfile.mobileconfig"]];

@SevaAlekseyev Чи допомогло вам це рішення коли-небудь?
Олег Дану

Ні, замість цього ми переосмислили всю архітектуру. Зараз є загальнодоступний зворотний проксі.
Сева Олексієв

2
Ви використовували це в призначеному додатку AppStore? Чи файл .mobileconfig вже був підписаний надійним сертифікатом o із самопідписаним сертифікатом? Я хотів би знати, чи може Apple відхилити програму, яка встановлює самопідписаний mobileconfig
mobileconfig iGenio

2
Чи можна перенаправити назад на додаток із браузера safari, як тільки ми встановимо наш конфігураційний профіль?
Ебілаш Баласубраманій

2
@igenio SmartJoin.us був прийнятий в App Store і використовує непідписаний профіль конфігурації, який він подає до Safari з веб-сервера в додатку
alfwatt

28

Відповідь від malinois працював у мене, АЛЕ, я хотів рішення, яке повертається до програми автоматично після того, як користувач встановив mobileconfig.

На це у мене пішло 4 години, але ось рішення, побудоване на ідеї Малінуа про наявність локального http-сервера: ви повертаєте HTML в сафарі, яке самооновлюється; перший раз сервер повертає mobileconfig, а другий раз повертає власну схему url, щоб повернутися до вашого додатку. UX - це те, що я хотів: додаток викликає сафарі, сафарі відкриває mobileconfig, коли користувач натискає "готово" на 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/"]];

Сподіваюся, це комусь допомагає.


1
якщо це працює, то приголомшливо. Дуже дякую. Я довго шукав рішення.
апарна

Я успішно цим користуюся. Можливо, вам доведеться відрегулювати інтервал часу перезавантаження - тут 400 мс
xaphod

Чи є інший спосіб повернутися до програми після інталяції? Користувач натискає кнопку "готово", і мені цікаво, чи є для цього обробник?
EmilDo

Якщо ви маєте на увазі власний інтерфейс для встановлення профілю та кнопку Готово вгорі праворуч, тоді ні, немає жодного загальнодоступного API, який дозволяє контролювати те, що я знаю.
xaphod

1
це дивно. спробую це. безумовно, величезний виграш для корпоративних програм.
ніндзянір

8

Я написав клас для встановлення файлу 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
        }
    }
}

Працював і у мене! Зверніть увагу, що наведений тут код працює лише для старішої версії Swifter.
Пітер Мейресоне

1
Я перетворив код на Swift 3 та останню версію Swifter. Працює досить добре, але виникають труднощі, коли система повертається до Safari, Safari перезавантажує сторінку (і потрапляє в цикл), а діалогове вікно для повернення до програми зникає (і Safari потрібно вбити). Дякую
Том

4
Мою редакцію додатка Swift 3 відхилили деякі розумні люди, які нічого не знають про ios та / або Swift (і різницю між версіями 2 та 3). Я помістив це в gist замість gist.github.com/3ph/beb43b4389bd627a271b1476a7622cc5 . Я знаю, що розміщення посилань суперечить ТАК, але, мабуть, і деякі люди.
Том,

1
Ваш суть справді чудова і колись працювала для мене. Після цього я повинен скинути кеш сафарі, щоб він знову запрацював. Мої тести з додатковою метаінформацією та полями заголовка не вирішили цього. Єдиним рішенням, яке я знайшов, є рандомізація "install" -String у setupHandlers ().
ObjectAlchemist

2
Гаразд, я вирішив свою проблему. Це працює лише зі схемою. Надання порожнього рядка як URL-адреси повернення призводить до описаної поведінки. З діючою схемою буде показано діалогове вікно, але після цього сафарі порушено (при скасуванні діалогового вікна). Замість повернення .MovedPermanently (self.returnURL) я пропоную повернути веб-сторінку з кнопкою всередині. У випадку помилки користувач може закрити сторінку ніж.
ObjectAlchemist

4

Я думаю, що ви шукаєте, це "Повітряна реєстрація", використовуючи Простий протокол реєстрації сертифікатів (SCEP). Погляньте на Посібник з реєстрації OTA та розділ SCEP щодо корисного навантаження в Посібнику з розгортання підприємств .

Відповідно до Огляду конфігурації пристрою у вас є лише чотири варіанти:

  • Настільна установка через USB
  • Електронна пошта (вкладення)
  • Веб-сайт (через Safari)
  • Реєстрація та розповсюдження в ефірі

Схоже на вишуканішу версію мого варіанту D :) Хоча хороша знахідка.
Сева Алексєєв

Є безліч продуктів, які роблять це за вас, не потрібно кататися самостійно
slf

1

Ви пробували просто, щоб програма надсилала користувачеві профіль конфігурації при першому запуску?

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

     [електронна пошта 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 = @ "Будь ласка, торкніться вкладення, щоб встановити конфігураційний профіль для My App.";
     [email setMessageBody: emailBody isHTML: YES];

     [self presentModalViewController: анімована електронна пошта: ТАК];
     [випуск електронної пошти];
}

Я зробив це IBAction на випадок, якщо ви хочете прив’язати його до кнопки, щоб користувач міг переслати його собі в будь-який час. Зверніть увагу, що у наведеному вище прикладі у мене може бути неправильний тип MIME, ви повинні це перевірити.


Спробує. Я не впевнений, що вкладення електронної пошти в процесі складання пошти можна відкрити. Крім того, інструктаж користувачів буде складним завданням. На ньому написано "відчайдушне рішення" ...
Сева Олексієв,

Користувач не відкривав вкладення під час створення. Робочим процесом буде запуск вашої програми, він зрозуміє, що профіль конфігурації не встановлений, він робить вищевказане, щоб ініціювати склад пошти, користувач вводить свою адресу електронної пошти та надсилає звернення. Потім вони відкривають програму Mail та завантажують електронний лист, натискаючи вкладення, щоб встановити його. Я погоджуюсь, що це здається відчайдушним обхідним шляхом. Крім того, ви можете зрозуміти, як Mail відправляє файл application / x-apple-aspen-config і просто зробити це (хоча це може бути приватний API, я не знаю).
smountcastle

1

Просто розмістіть файл на веб-сайті з розширенням * .mobileconfig і встановіть тип MIME на application / x-apple-aspen-config. Користувачеві буде запропоновано, але якщо він погодиться, профіль слід встановити.

Ви не можете встановити ці профілі програмно.


0

Ця сторінка пояснюється, як використовувати зображення зі свого набору в UIWebView.

Можливо, те саме працює і для профілю конфігурації.


1
Ні. І найцікавіше те, що це якось специфіка профілю. Коли я надаю текстовий файл із посиланням на нього, UIWebView переходить до нього, як очікувалося.
Сева Алексєєв

0

У мене є інший спосіб, яким це може працювати (на жаль, у мене немає профілю конфігурації для тестування):

// Створюємо UIViewController, який містить UIWebView
- (void) viewDidLoad {
    [супер viewDidLoad];
    // Розповідає webView про завантаження конфігураційного профілю
    [self.webView loadRequest: [NSURLRequest requestWithURL: self.cpUrl]];
}

// Тоді у вашому коді, коли ви побачите, що профіль не встановлений:
ConfigProfileViewController * cpVC = 
        [[ConfigProfileViewController alloc] initWithNibName: @ "MobileConfigView"
                                                      розшарування: нуль];
NSString * cpPath = [[NSBundle mainBundle] pathForResource: @ "configProfileName"
                                                   ofType: @ ". mobileconfig"];
cpVC.cpURL = [URL-адреса NSURLWithString: cpPath];
// Тоді, якщо у вашому додатку є навігаційний контролер, ви можете просто натиснути подання 
// увімкнено, і він завантажить ваш мобільний конфігуратор (який повинен його встановити).
[self.navigationController pushViewController: контролер анімований: ТАК];
[випуск 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;
}

В іншому випадку ви можете розглянути можливість увімкнення встановлення із захищеного сервера.


Це питання все ще залишається без відповіді?
Хоанг Фам,

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 зробили брудну роботу?
Охад Коен,

Профіль конфігурації робить важку роботу, не вимагаючи приватних API. Брудним є процес проходження користувача через встановлення профілю з програми - iOS може обробляти файл лише з Safari або Mail, це не зовсім найлегший досвід, і повернення користувача до програми після цього є додатковою забавою.
Еліот Гіллум,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.