.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ń