.NET Framework
pozwala na łatwe wczytywanie, uruchamianie i wykorzystywanie obcych
assembly. Należy jednak pamiętać, że nie wolno mieć pełnego
zaufania do cudzego kodu. Ma to jeszcze większe znaczenie gdy
dopuszczamy sytuację, w której pozwalamy na wykonywanie kodu,
którego nigdy nie testowaliśmy, z którym nigdy nie mieliśmy do
czynienia. Oczywiście w takim przypadku otrzymujemy bardzo dobre
wsparcie ze strony .NET Framework, a mianowicie obsługę tzw. Domen
aplikacji.
Domena aplikacji (ang. Application Domain), czyli AppDomain, jest logiczną
jednostką wykonawczą. Jest to niejako „piaskownica”, w której
wykonuje się kod .NET. Domena aplikacji zapewnia kontener, w którym
kod może się bezpiecznie wykonywać, ze świadomością, że
programy pracujące poza jego domeną aplikacji nie mogą negatywnie
na niego wpłynąć. [1]
Tym razem, w nawiązaniu do przykładu
z prostą obsługą wtyczek (plugin'ów), chciałbym pokazać jak
można wykorzystać mechanizm domen w .NET. Dodam więc pewne
zabezpieczenia do wcześniejszego
kodu. Tutaj należy jeszcze dodać [1], że w przypadku
komunikacji między domenowej (a z taką będziemy mieć do
czynienia), należy wykorzystać komunikację międzyprocesową, np.
z wykorzystaniem Remoting. To właśnie .NET Remoting zostanie
wykorzystany do wykorzystania (zdalnego) obiektu w innej domenie.
W związku z
wykorzystaniem .NET Remoting'u należy do wcześniejszego kodu
wprowadzić pewne zmiany:
- klasa wykorzystywana poprzez wtyczkę (w przykładzie Class1) musi być serializowalna (oznaczamy ją więc atrybutem [Serializable]);
- należy zrezygnować z obsługi wtyczki poprzez interfejs, a zamiast tego wykorzystać klasę abstracyjną, która będzie dziedziczyła po MarshalByRefObject)
Ponadto przykład
został rozbudowany o następujące elementy:
- obsługę wyjątków i wyświetlanie informacji o nich do użytkownika;
- do wtyczki zostanie wprowadzony „zły” kod, który zwraca jako łańcuch tekstowy zawartość pliku win.ini;
- w aplikacji obsługującej wtyczki zostanie stworzona domena MyNewSecureDomain, do której ładowane będą pliki DLL z wtyczkami;
- do utworzonej domeny nowe assembly będą ładowane z wykorzystaniem zabezpieczeń odpowiadających kodu pobranego z internetu (SecurityZone.Internet).
Ostatecznie mamy
następujący kod (polecam przeczytanie komentarzy, które
uzupełniają powyższy opis).
Class1.cs:
using System; using System.IO; using ChangeSupportFramework; namespace PluginLibrary1 { [Serializable] //this remote object must be serializable public class Class1 : DoSthAbstract { #region IDoSth Members public override string DoSth() { string winini = ""; using (StreamReader reader = new StreamReader(@"C:\Windows\win.ini")) { winini = reader.ReadToEnd(); } return " Plugin Class, Win.ini: " + winini; } #endregion } }
Program.cs:
using System; using System.IO; using System.Reflection; using System.Runtime.Remoting; using System.Security; using System.Security.Policy; namespace ChangeSupportFramework { public abstract class DoSthAbstract : MarshalByRefObject //this is important that this class is MarshalByRefObject { public abstract string DoSth(); } class Program { static void Main() { //selecting current directory DirectoryInfo di = new DirectoryInfo("."); //checking all DLL files in the current directory that start from "Plugin" foreach (FileInfo fi in di.GetFiles("Plugin*.dll")) { try { //creating evidence for the new domain //we are trusting plugin as any internet application, our security setting //are base on configured setting in the operating system: SecurityZone.Internet object[] hostEvidence = { new Zone(SecurityZone.Internet) }; Evidence internetEvidence = new Evidence(hostEvidence, null); //creation of new domain: MyNewSecureDomain AppDomain myDomain = AppDomain.CreateDomain("MyNewSecureDomain"); //creation of Assembly Name based on assembly file from "Plugin*.dll" set AssemblyName asname = AssemblyName.GetAssemblyName(fi.FullName); //loading of the Plugin to particular Domain with Internet Zone restriction Assembly pluginAssembly = myDomain.Load(asname, internetEvidence); //checking all public types from loaded assembly foreach (Type pluginType in pluginAssembly.GetExportedTypes()) { //now we are checking whether particular type implements //IDoSth interface if (pluginType.IsSubclassOf(typeof(DoSthAbstract)))// != null) { //creating the instance of selected type //IMPORTANT: we have to passed evidence here ObjectHandle OHpluginType = myDomain.CreateInstance(asname.FullName, pluginType.FullName); //here is an important place, it is possible to use CreateInstance as is it shown above //this is because we have already leaded the assembly to secure domain created before //however it is possible not to load the assebly ealier and pass the secure evidence now: //ObjectHandle OHpluginType = myDomain.CreateInstance(asname.FullName, pluginType.FullName), // false, 0, null, null, null, null, internetEvidence); //to use remote object we have to Unwrap it //see: http://msdn.microsoft.com/en-us/library/system.runtime.remoting.objecthandle.aspx DoSthAbstract TypeLoadedFromPlugin = (DoSthAbstract)OHpluginType.Unwrap(); //do sht with the plugin Console.WriteLine(TypeLoadedFromPlugin.DoSth()); } } } catch (Exception ex) { Console.WriteLine("Cannot use plugin: {0}, the reason is: {1}", fi.Name, ex.Message); } } Console.ReadKey(); } } }
Po uruchomieniu możemy podziwiać ekran z wyjątkiem:
Zamiast (w
przypadku niewykorzystania zabezpieczeń):
Co jeszcze?
Powyższy mechanizm można wykorzystać nie tylko do ładowania i wykorzystania wtyczek, ale też do uruchomiania innych aplikacji z poziomu naszej aplikacji, w tym celu należy wykorzystać metodę ExecuteAssembly klasy AppDomain, na przykład jak to pokazano poniżej:object[] hostEvidence = { new Zone(SecurityZone.Internet) }; Evidence internetEvidence = new Evidence(hostEvidence, null); AppDomain myDomain = AppDomain.CreateDomain("MyDomain"); myDomain.ExecuteAssembly("PluginLibrary1AppUser.exe", internetEvidence);
Na koniec kubeł zimnej wody
Dla tych, którym
rozwiązanie się spodobało, dla ochłody informacja:
Powyższy kod
nie zadziała dla .NET 4.0, a otrzymamy tylko:
This method
implicitly uses CAS policy, which has been obsoleted by the .NET
Framework. In order to enable CAS policy for compatibility reasons,
please use the NetFx40_LegacySecurityPolicy configuration switch.
Please see http://go.microsoft.com/fwlink/?LinkID=155570
for more information.
Można to obejść
konfigurując app.config, jak pokazano niżej, ale chyba nie o to
chodzi...
<configuration> <runtime> <NetFx40_LegacySecurityPolicy enabled="true"/> </runtime> </configuration>
Postaram się kiedyś wrócić do tematu i opisać jak to powinno być
wykonane w .NET Framework 4.0.
Literatura
[1] "Microsoft
Visual C# 2005 Księga eksperta", Autor: Kevin HoffMan,
Wydawnictwo: Helion, 2007
[3] „Podsumowanie
wpisów przygotowujących do egzaminu 70-536”, Eastgroup.pl
[4] „MCTS
Self-Paced Training Kit (Exam 70-536): Microsoft .NET Framework
Application Development Foundation 2nd edition”, Autor: Tony
Northrup , Wydawnictwo: Microsoft Press, 2009
Super jest ten wpis
OdpowiedzUsuń