HOW-TO: Sidebar Gadgets - Part I

How-to: Appeler un service web depuis un Gadget Sidebar

Besoin: Envoyer des informations vers un serveur pour déclencher un traitement distant et récupérer le résultat du traitement depuis le gadget sidebar.

Solution: Il est possible d'appeler un service Web depuis un Gadget sidebar pour déclencher un traitement et récupérer un résultat. Cette solution est décrite sur le blog de Cyril Durand, l'article Gadget Vista, consommer un webservice avec Atlas. Un tutoriel est aussi disponible en anglais expliquant pas-à-pas l'implémentation du service Web, sa configuration et l'implémentation de l'appel au service Web en Javascript.

How-to: Appeler une classe .NET depuis un Gadget Sidebar (sans regasm.exe)

Article traduit en anglais.  

Besoin: Dans certains cas, le développeur de gadget souhaiterait échanger des données avec un autre module tel qu'une librairie de calcul ou un programme réalisant un traitement.

Dans le cas d'une librairie, nous savons qu'il est possible d'accéder à des composants COM en utilisant le constructeur ActiveXObject (exemple: var xmldoc = new ActiveXObject("Microsoft.XMLDOM");).

Il existe d'autres moyens d'interopérer avec un programme ou un site via une url de service Web par exemple comme l'exemple l'article en anglais de Karsten Januszewski sur WCF

Ou en référançant un contrôle Windows Forms avec le tag OBJECT et un CLASSID utilisant une url#type - type étant un full type (namespace + nom de la classe). Un exemple ici. Note: ce principe ne fonctionne pas pour un assembly local (répertoire local).

De même, il est tout à fait possible d'exécuter un programme en ligne de commande et de récupérer la sortie console de ce programme. De ce fait, l'exécution en ligne de commandes permet de passer des paramètres à un programme et de récupérer le résultat du traitement sur la sortie standard.

Concernant la solution, nous revenons sur le premier point à savoir interopérer avec un assembly .NET exposé en COM. Mais cette solution présente l'inconvénient de devoir enregistrer l'assembly dans la base des registres afin qu'il soit utilisable comme tout composant COM. La solution décrite ci-dessous permet d'enregistrer dynamiquement en Javascript un assembly .NET.

Solution: Le principe retenu pour appeler une classe .NET depuis un Gadget Sidebar est d'exposer l'assembly .NET sous forme de composant COM. Le déploiement nécessite dans ce cas le droit d'installation des applications par l'utilisateur et le contrôle d'accès utilisateur (UAC - User Account Control) avertira l'utilisateur de l'enregistrement des composants dans la base des registres. Autre solution à vérifier (non testée); L'écriture de fichier Manifest (ou la génération avec l'outil MT) permettant l'utilisation de composants COM sans enregistrement (possible depuis Windows XP). Mais cette piste reste à tester.

La solution présentée ici permet de contourner les problèmes évoqués ci-dessus lors du déploiement tout en conservant un modèle de programmation connu et homogène. La première étape consiste à exposer l'assembly .NET en COM (je recommande la lecture de l'article de Phil Wilson en anglais). L'étape d'enregistrement de l'assembly .NET en composant COM est normalement faite via la commande regasm.exe. Sachant que cette commande insère des entrées dans la base des registres dans HKEY-CLASSES_ROOT, l'enregistrement est impossible lors du déploiement pour tout utilisateur non administrateur de la machine (accès restreint à la base des registres). Par contre, un utilisateur peut tout à fait accéder à la base des registres par la clé HKEY_CURRENT_USER (HKCU), ce qui est d'ailleurs fait par un programme d'installation lorsque l'utilisateur choisit une installation personnelle. L'idée venue d'un discussion avec Simon Mourier, directeur technique de la société SoftFluent, était de simuler le comportement de regasm sur la clé HKCU. En examinant de près les options de la commande regasm, vous verrez qu'elle prend plusieurs arguments dont /regfile qui permet de générer le fichier .reg plutôt que d'enregistrer l'assembly en base des registrers.

Le fichier .reg généré par la commande regasm.exe est le suivant (notez l'utilisation de l'option /codebase de la commande):

REGEDIT4

[HKEY_CLASSES_ROOT\DPAPIComLib.DPAPIUserScopeCcw]
@="MTC.Security.DPAPIUserScopeCcw"

[HKEY_CLASSES_ROOT\DPAPIComLib.DPAPIUserScopeCcw\CLSID]
@="{C76D54A7-8472-44C7-AD7F-BA394E1D3900}"

[HKEY_CLASSES_ROOT\CLSID\{C76D54A7-8472-44C7-AD7F-BA394E1D3900}]
@="MTC.Security.DPAPIUserScopeCcw"

[HKEY_CLASSES_ROOT\CLSID\{C76D54A7-8472-44C7-AD7F-BA394E1D3900}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="MTC.Security.DPAPIUserScopeCcw"
"Assembly"="DPAPIComLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3def732129791e7a"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="file:///C:/Users/fredeq/AppData/Local/Microsoft/Windows Sidebar/Gadgets/mydotnet[1].gadget/bin/DPAPIComLib.dll"

[HKEY_CLASSES_ROOT\CLSID\{C76D54A7-8472-44C7-AD7F-BA394E1D3900}\InprocServer32\1.0.0.0]
"Class"="MTC.Security.DPAPIUserScopeCcw"
"Assembly"="DPAPIComLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3def732129791e7a"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="file:///C:/Users/fredeq/AppData/Local/Microsoft/Windows Sidebar/Gadgets/mydotnet[1].gadget/bin/DPAPIComLib.dll"

[HKEY_CLASSES_ROOT\CLSID\{C76D54A7-8472-44C7-AD7F-BA394E1D3900}\ProgId]
@="DPAPIComLib.DPAPIUserScopeCcw"

[HKEY_CLASSES_ROOT\CLSID\{C76D54A7-8472-44C7-AD7F-BA394E1D3900}\Implemented Categories\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]

 Le principe d'enregistrement de composants COM pour un utilisateur tient au simple fait que les entrées ci-dessus ne se font pas dans HKCR mais dans HKCU\Software\Classes. Il suffit donc de remplacer HKEY_CLASSES_ROOT par HKEY_CURRENT_USER\Software\Classes pour obtenir une entrée équivalente et un comportement d'instanciation identique. Pour mieux comprendre l'incidence des permissions utilisateur sur la base des registres, je recommande la lecture de l'article en anglais sur le site MSDN intitulé Developing Software in Visual Studio .NET with Non-Administrative Privileges, la partie Developing software/Doing post-build steps (registration).

Où en sommes-nous? Nous avons: un assembly .NET exposé en COM, une méthode manuelle d'enregistrement type regasm mais sur la clé HKCU, la possibilité d'appeler des composants COM en Javascript et donc le composant WScript (WScript.Shell) pour modifier la base des registres. Nous pouvons donc coder la méthode manuelle pour enregistrer dynamiquement notre assembly .NET.

Le code est le suivant:

function regAsm(root, progId, cls, clsid, assembly, version, codebase) {
var wshShell;
wshShell = new ActiveXObject("WScript.Shell");

//wshShell.RegWrite(root + "\\Software\\Classes\\", progId);
wshShell.RegWrite(root + "\\Software\\Classes\\" + progId + "\\", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\" + progId + "\\CLSID\\", clsid);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\", cls);

wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\", "mscoree.dll");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\ThreadingModel", "Both");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\Class", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\Assembly", assembly);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\RuntimeVersion", "v2.0.50727");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\CodeBase", codebase);

wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\" + version + "\\Class", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\" + version + "\\Assembly", assembly);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\" + version + "\\RuntimeVersion", "v2.0.50727");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\InprocServer32\\" + version + "\\CodeBase", codebase);

wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\ProgId\\", progId);

wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid + "\\Implemented Categories\\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}\\", "");
}

L'appel de cette fonction se fait de la manière suivante:

function button1_click()
{
regAsm("HKCU",
"DPAPIComLib.DPAPIUserScopeCcw",
"MTC.Security.DPAPIUserScopeCcw",
"{C76D54A7-8472-44C7-AD7F-BA394E1D3900}",
"DPAPIComLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3def732129791e7a",
"1.0.0.0",
System.Gadget.path + "/bin/DPAPIComLib.dll");
}

A noter que dans l'exemple ci-dessus il est possible de remplacer HKCU par HKLM (ce qui est possible dans le cas d'un utilisateur ayant les permissions Administrateur de la machine).

Prochaines étapes:

- Vous avez certainement remarqué le nom de la classe dans cet exemple: DPAPI...; La prochaine étape consistera à l'écriture d'un "how-to" sur la cryptage des données d'un Gadget.

Le code utilisé pour cet article est le suivant:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;

namespace MTC.Security
{
[ComVisible(true)]
[Guid("403F88FB-AF62-4acd-989F-C15B56436CBF")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IDPAPIUserScopeCcw
{
string Protect(string data, string entropy);
string Unprotect(string securedBase64Data, string entropy);
}

/// <summary>
/// Protect/Unprotect data.
/// </summary>
[ComVisible(true)]
[Guid("C76D54A7-8472-44c7-AD7F-BA394E1D3900")]
[ProgId("DPAPIComLib.DPAPIUserScopeCcw")]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IDPAPIUserScopeCcw))]
public sealed class DPAPIUserScopeCcw : IDPAPIUserScopeCcw
{
/// <summary>
/// Protect
/// </summary>
/// <param name="data">Clear data.</param>
/// <param name="entropy">Entropy used to protect.</param>
/// <returns>A base 64 string containing the secured data.</returns>
public string Protect(string data, string entropy)
{
byte[] dataBytes = Encoding.Unicode.GetBytes(data);
byte[] entropyBytes = Encoding.Unicode.GetBytes(entropy);

byte[] encryptedBytes = ProtectedData.Protect(dataBytes, entropyBytes, DataProtectionScope.CurrentUser);
return Convert.ToBase64String(encryptedBytes);
}

/// <summary>
/// Unprotect
/// </summary>
/// <param name="securedBase64Data">A base 64 secured data.</param>
/// <param name="entropy">Entropy used to unprotect.</param>
/// <returns>Clear data.</returns>
public string Unprotect(string securedBase64Data, string entropy)
{
byte[] encryptedBytes = Convert.FromBase64String(securedBase64Data);// (securedData);
byte[] entropyBytes = Encoding.Unicode.GetBytes(entropy);

byte[] dataBytes = ProtectedData.Unprotect(encryptedBytes, entropyBytes, DataProtectionScope.CurrentUser);
return Encoding.Unicode.GetString(dataBytes);
}
}
}