web-dev-qa-db-fra.com

Installation d'un profil de configuration sur iPhone - par programme

J'aimerais envoyer un profil de configuration avec mon application iPhone et l'installer si nécessaire. 

Remarquez que nous parlons d'un profil de configuration, pas d'un profil d'approvisionnement.

Tout d'abord, une telle tâche est possible. Si vous placez un profil de configuration sur une page Web et que vous cliquez dessus à partir de Safari, celui-ci sera installé. Si vous envoyez un profil par courrier électronique et cliquez sur la pièce jointe, celui-ci sera également installé. "Installé" dans ce cas signifie "l'interface utilisateur d'installation est invoquée" - mais je ne pouvais même pas aller aussi loin.

Je travaillais donc selon la théorie voulant que l’installation d’un profil implique sa navigation en tant qu’URL. J'ai ajouté le profil à mon ensemble d'applications.

A) Tout d'abord, j'ai essayé [sharedApp openURL] avec le fichier: // URL dans mon lot. Pas de chance, rien ne se passe.

B) J'ai ensuite ajouté une page HTML à mon paquet contenant un lien vers le profil et je l'ai chargé dans un UIWebView. En cliquant sur le lien ne fait rien. Le chargement d'une page identique à partir d'un serveur Web dans Safari fonctionne toutefois correctement - le lien est cliquable, le profil est installé. J'ai fourni un UIWebViewDelegate, répondant OUI à chaque demande de navigation - aucune différence.

C) Ensuite, j'ai essayé de charger la même page Web à partir de mon ensemble dans Safari (avec [sharedApp openURL] - rien ne se produit. Safari ne peut pas voir les fichiers contenus dans mon ensemble d'applications.

D) Le téléchargement de la page et du profil sur un serveur Web est faisable, mais constitue une gêne pour l'organisation, sans parler d'une source supplémentaire de défaillances (que se passe-t-il si aucune couverture 3G? Etc.).

Donc ma grande question est: ** comment installer un profil par programme?

Et les petites questions sont: qu'est-ce qui peut rendre un lien non cliquable dans un UIWebView? Est-il possible de charger un fichier: // URL à partir de mon bundle dans Safari? Sinon, y a-t-il un emplacement local sur iPhone où je peux placer des fichiers et que Safari peut les trouver?

EDIT sur B): le problème réside dans le fait que nous établissons un lien vers un profil. Je l'ai renommé de .mobileconfig à .xml (parce que c'est vraiment du XML), j'ai modifié le lien. Et le lien a fonctionné dans mon UIWebView. Renommé - les mêmes choses. Il semble que UIWebView soit réticent à faire des choses à l'échelle de l'application - puisque l'installation du profil ferme l'application. J'ai essayé de lui dire que tout allait bien - à l'aide de UIWebViewDelegate - mais cela n'a pas convaincu. Même comportement pour mailto: URL dans UIWebView.

Pour les URL mailto:, la technique courante consiste à les traduire en appels [openURL], mais cela ne fonctionne pas très bien dans mon cas, voir le scénario A.

Pour itms: URL, cependant, UIWebView fonctionne comme prévu ...

EDIT2: a essayé de fournir une URL de données à Safari via [openURL] - ne fonctionne pas, voir ici: iPhone Open DATA: URL dans Safari

EDIT3: a trouvé de nombreuses informations sur la manière dont Safari ne prend pas en charge les URL fichier: //. UIWebView, cependant, le fait beaucoup. En outre, Safari sur le simulateur les ouvre parfaitement. Le dernier bit est le plus frustrant.


EDIT4: Je n'ai jamais trouvé de solution. Au lieu de cela, j'ai mis en place une interface Web à deux bits dans laquelle les utilisateurs peuvent commander le profil par courrier électronique.

69
Seva Alekseyev

1) Installez un serveur local comme RoutingHTTPServer

2) Configurez l'en-tête personnalisé: 

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

3) Configurez le chemin racine local pour le fichier mobileconfig (Documents):

[httpServer setDocumentRoot:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]];

4) Afin de laisser le temps au serveur Web d’envoyer le fichier, ajoutez ceci:

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) Dans votre contrôleur, appelez safari avec le nom du mobileconfig enregistré dans Documents:

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

La réponse du malinois a fonctionné pour moi, MAIS, je voulais une solution qui revienne automatiquement à l'application après que l'utilisateur ait installé le mobileconfig.

Cela m'a pris 4 heures, mais voici la solution, construite sur l’idée malinoise de disposer d’un serveur http local: vous renvoyez du code HTML à un safari qui se rafraîchit lui-même; la première fois que le serveur renvoie mobileconfig, et la deuxième fois, le schéma url personnalisé pour revenir à votre application. L'UX est ce que je voulais: l'application appelle safari, safari ouvre mobileconfig, lorsque l'utilisateur clique "done" sur mobileconfig, puis safari charge à nouveau votre application (schéma d'URL personnalisé).

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

... et voici le code à appeler depuis l'application (c.-à-d. viewcontroller):

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

J'espère que ça aide quelqu'un.

27
xaphod

J'ai écrit une classe pour installer un fichier mobileconfig via Safari puis revenir à l'application. Il s’appuie sur le moteur du serveur http Swifter qui, selon moi, fonctionne bien . Je souhaite partager mon code ci-dessous pour ce faire. Il est inspiré par plusieurs sources de code que j'ai trouvées flottant dans le www. Donc, si vous trouvez des morceaux de votre propre code, des contributions pour vous.

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
        }
    }
}
6
freshking

Je pense que ce que vous recherchez, c’est «Inscription à la radio» en utilisant le protocole SCEP (Simple Certificate Enrollment Protocol). Consultez le OTA Enrollment Guide et la section SCEP Payload du Enterprise Deployment Guide .

Selon la Device Config Overview , vous n’avez que quatre options:

  • Installation de bureau via USB
  • Pièce jointe)
  • Site Web (via Safari)
  • Inscription et distribution en direct
4
slf

Hébergez simplement le fichier sur un site Web portant l'extension * .mobileconfig et définissez le type MIME sur application/x-Apple-aspen-config. L'utilisateur sera invité, mais s'il accepte, le profil doit être installé.

Vous ne pouvez pas installer ces profils par programme.

0
Brandon

C'est un bon fil, et en particulier le blog mentionné ci-dessus .

Pour ceux qui pratiquent le Xamarin, voici mes 2 centimes supplémentaires. J'ai intégré le certificat feuille dans mon application en tant que contenu, puis j'ai utilisé le code suivant pour le vérifier:

        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

(Man, j'adore la pureté de ce code, par rapport aux langages d'Apple)

0
Eliot Gillum

Vous ne savez pas pourquoi vous avez besoin d'un profil de configuration, mais vous pouvez essayer de pirater ce délégué depuis UIWebView:

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

Sinon, vous pouvez envisager d’activer l’installation à partir d’un serveur sécurisé.

0
Hoang Pham

J'ai pensé à une autre façon dont cela pourrait fonctionner (malheureusement, je n'ai pas de profil de configuration à tester):

 // Créer un UIViewController contenant un UIWebView 
- (void) viewDidLoad {
 [super viewDidLoad]; 
 // Indique à la WebView de charger le profil de configuration 
 [self.webView loadRequest: [NSURLRequest requestWithURL: self.cpUrl]]; 
} 

 // Ensuite, dans votre code lorsque vous voyez que le profil n'a pas été installé: 
 ConfigProfileViewController * cpVC = 
 [[ConfigProfileViewController alloc] initWithNibName: @ "MobileConfigView" 
 bundle: nil]; 
 NSString * cpPath = [[NSBundle mainBundle] pathForResource: @ "configProfileName" 
 ofType: @ ". mobileconfig"]; 
 cpVC.cpURL = [NSURL URLWithString: cpPath]; 
 // Ensuite, si votre application dispose d'un contrôleur de navigation, vous pouvez simplement pousser la vue 
 // sur et sur va charger votre config mobile (qui devrait l'installer) .
 [self.navigationController pushViewController: contrôleur animé: OUI]; 
 [version cpVC]; 
0
smountcastle

Avez-vous essayé simplement de faire en sorte que l'application envoie un courrier électronique à l'utilisateur le profil de configuration la première fois qu'il démarre?

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

 [email setSubject: @ "Profil de configuration de mon application"]; 

 NSString * filePath = [[NSBundle mainBundle] pathForResource: @ "MyAppConfig" ofType: @ "mobileconfig"]; 
 NSData * configData = [NSData dataWithContentsOfFile: filePath]; 
 [email addAttachmentData: configData type mime: @ "application/x-Apple-aspen-config" nomFichier: @ "MyAppConfig.mobileconfig"]; 

 NSString * emailBody = @ "Veuillez cliquer sur la pièce jointe pour installer le profil de configuration de My App."; 
 [email setMessageBody: emailBody isHTML: YES]; 

 [self presentModalViewController: e-mail animé: OUI]; 
 [email release]; 
} 

J'en ai fait un IBAction au cas où vous voudriez le lier à un bouton pour que l'utilisateur puisse le renvoyer à tout moment. Notez que je peux ne pas avoir le type MIME correct dans l'exemple ci-dessus, vous devriez le vérifier.

0
smountcastle

Cette page explique comment utiliser les images de votre lot dans un UIWebView.

Peut-être que la même chose fonctionnerait aussi pour un profil de configuration.

0
Jon-Eric