Frage

Ich bin auf der Suche nach einer einfachen und sicheren Weg, um Zugang zu Plugins aus einer .NET-Anwendung. Obwohl ich mir vorstellen, dass dies eine sehr häufige Anforderung, ich habe Schwierigkeiten, etwas zu finden, das alle meine Bedürfnisse erfüllt:

  • Die Host-Anwendung werden entdecken, und seine Plug-Baugruppen zur Laufzeit laden
  • Plugins wird von unbekannten dritten Parteien erstellt werden, so dass sie Sandbox werden müssen, um sie zu verhindern, dass bösartiger Code ausgeführt
  • Eine gemeinsame Interop werden Montagearten enthalten, die sowohl vom Host und seine Plugins referenziert werden
  • Jedes Plugin Baugruppe enthält eine oder mehrere Klassen, die eine gemeinsame Plugin-Schnittstelle
  • implementieren
  • Wenn ein Plugin-Instanz initialisieren, wird der Host es einen Verweis auf sich selbst in Form einer Host-Schnittstelle
  • passieren
  • Der Host in das Plugin über seine gemeinsame Schnittstelle aufruft und die Plugins in den Host anrufen kann ebenfalls
  • Der Host und die Plugins werden Daten in Form der Typen austauschen Montag in der Interop definiert (einschließlich generischen Typen)

Ich habe sowohl MEF und MAF untersucht, aber ich bin kämpfen, um zu sehen, wie einer von ihnen gemacht werden kann, um die Rechnung zu passen.

Unter der Annahme, mein Verständnis richtig ist, ist MAF nicht in der Lage die Weitergabe von generischen Typen über ihre Isolationsgrenze zu unterstützen, die zu meiner Software. (MAF ist auch sehr komplex zu implementieren, aber ich würde mit diesem zu arbeiten vorbereitet werden, wenn ich das generischen Typ Problem lösen könnte).

MEF ist fast eine perfekte Lösung, erscheint aber kurz auf die Sicherheitsanforderung fallen, da es seine Verlängerung Baugruppen in derselben AppDomain als Host lädt und somit scheinbar Sandbox verhindert.

Ich habe diese Frage gesehen, die in einer Sandbox-Modus laufen MEF spricht, beschreibt aber nicht, wie. Dieser Beitrag heißt es, dass „wenn MEF verwenden, müssen Sie Vertrauen Erweiterungen nicht bösartigen Code oder bietet Schutz über Codezugriffssicherheit“, sondern wieder zu laufen, ist es nicht beschreiben, wie. Schließlich gibt es hier . Aber soweit ich sehen kann, nur diese Technik erlaubt es mir, spät zu verwenden Bindung statische Methoden auf Klassen in einer nicht vertrauenswürdigen Versammlung berufen. Wenn ich versuche, diesen Ansatz zu erweitern, eine Instanz eines meiner Plugin Klassen zu erstellen, kann die zurückgegebene Instanz nicht an die gemeinsamen Plugin-Schnittstelle umgewandelt werden, was bedeutet, dass es unmöglich ist, für die Host-Anwendung in sie zu nennen. Gibt es eine Technik, die ich verwenden kann, um stark typisierte Proxy-Zugriff über die AppDomain Grenze?

zu erhalten

Ich entschuldige mich für die Länge dieser Frage; der Grund war, alle Wege zu zeigen, dass ich bereits untersucht habe, in der Hoffnung, dass jemand etwas Neues vorschlagen kann, um zu versuchen.

Vielen Dank für Ihre Idees, Tim

War es hilfreich?

Lösung

Weil du in verschiedenen AppDomains sind, können Sie nicht nur die Instanz übergeben über.

Sie müssen Ihre Plug-Ins Remotable machen und einen Proxy in Ihrem Haupt-App erstellen. Werfen Sie einen Blick auf die Dokumentation für CreateInstanceAndUnwrap , die ein Beispiel hat, wie dies alles funktionieren könnte der Boden.

Dies ist auch ein weiterer viel breiter Übersicht von Jon Shemitz die ich denke, gut zu lesen ist. Viel Glück.

Andere Tipps

Ich habe Alastair Maw Antwort akzeptiert, da er seinen Vorschlag und Links, die mich zu einer praktikablen Lösung geführt, aber ich bin Entsendung hier einige Details genau, was ich tat, für alle anderen, die versuchen können, etwas Ähnliches zu erreichen.

Zur Erinnerung: in seiner einfachstenen Form meiner Anwendung besteht aus drei Baugruppen:

  • Die Hauptanwendung verbrauchen, dass wird die Montage Plugins
  • Eine Interopassembly, die definiert, gängige Typen von der Anwendung gemeinsam genutzt und seine Plugins
  • Ein Beispiel-Plug-Montage

Der folgende Code ist eine vereinfachte Version meines echten Code, zeigte nur das, was erforderlich ist, zu entdecken und Last-Plugins, die jeweils in einem eigenen AppDomain:

mit der Hauptanwendung starten Montage verwendet das Hauptprogramm Klasse eine Hilfsklasse namens PluginFinder Plugin-Typen innerhalb beliebiger Baugruppen in einem bestimmten Plugin-Ordner zu entdecken Qualifizierte. Für jeden dieser Typen, schafft es dann eine Instanz eines sandox AppDomain (mit Internet-Zone Berechtigungen) und verwendet sie eine Instanz des entdeckten Plugin-Typ zu erstellen.

Wenn Sie einen AppDomain mit eingeschränkten Berechtigungen zu schaffen, ist es möglich, einen oder mehr vertrauenswürdigen Baugruppen zu spezifizieren, die nicht unter diesen Berechtigungen sind. Um dies zu erreichen in dem Szenario präsentiert hier die Hauptanwendung Montage und ihre Abhängigkeiten (die Interop-Assembly) muss unterzeichnet werden.

Für jede geladene Plugin Beispiel die benutzerdefinierten Methoden innerhalb des Plugins kann über seine bekannte Schnittstelle und das Plugin aufrufen kann auch über seine bekannte Schnittstelle zum Host-Anwendung aufgerufen werden zurück. Schließlich wird die Host-Anwendung entlädt jede der Sandbox-Domänen.

class Program
{
    static void Main()
    {
        var domains = new List<AppDomain>();
        var plugins = new List<PluginBase>();
        var types = PluginFinder.FindPlugins();
        var host = new Host();

        foreach (var type in types)
        {
            var domain = CreateSandboxDomain("Sandbox Domain", PluginFinder.PluginPath, SecurityZone.Internet);
            plugins.Add((PluginBase)domain.CreateInstanceAndUnwrap(type.AssemblyName, type.TypeName));
            domains.Add(domain);
        }

        foreach (var plugin in plugins)
        {
            plugin.Initialize(host);
            plugin.SaySomething();
            plugin.CallBackToHost();

            // To prove that the sandbox security is working we can call a plugin method that does something
            // dangerous, which throws an exception because the plugin assembly has insufficient permissions.
            //plugin.DoSomethingDangerous();
        }

        foreach (var domain in domains)
        {
            AppDomain.Unload(domain);
        }

        Console.ReadLine();
    }

    /// <summary>
    /// Returns a new <see cref="AppDomain"/> according to the specified criteria.
    /// </summary>
    /// <param name="name">The name to be assigned to the new instance.</param>
    /// <param name="path">The root folder path in which assemblies will be resolved.</param>
    /// <param name="zone">A <see cref="SecurityZone"/> that determines the permission set to be assigned to this instance.</param>
    /// <returns></returns>
    public static AppDomain CreateSandboxDomain(
        string name,
        string path,
        SecurityZone zone)
    {
        var setup = new AppDomainSetup { ApplicationBase = Path.GetFullPath(path) };

        var evidence = new Evidence();
        evidence.AddHostEvidence(new Zone(zone));
        var permissions = SecurityManager.GetStandardSandbox(evidence);

        var strongName = typeof(Program).Assembly.Evidence.GetHostEvidence<StrongName>();

        return AppDomain.CreateDomain(name, null, setup, permissions, strongName);
    }
}

In diesem Beispielcode, die Host-Anwendungsklasse ist sehr einfach, Aussetzen nur eine Methode, die von Plug-in aufgerufen werden kann. Allerdings muss diese Klasse von MarshalByRefObject ableiten, so dass es zwischen den Anwendungsdomänen referenziert werden.

/// <summary>
/// The host class that exposes functionality that plugins may call.
/// </summary>
public class Host : MarshalByRefObject, IHost
{
    public void SaySomething()
    {
        Console.WriteLine("This is the host executing a method invoked by a plugin");
    }
}

Die PluginFinder Klasse hat nur eine öffentliche Methode, dass die Renditen eine Liste der gefundenen Plugin-Typen. Diese Entdeckung Prozess lädt jede Versammlung, die er findet und nutzt Reflexion seine Qualifikationstypen zu identifizieren. Da dieser Prozess möglicherweise viele Baugruppen geladen werden kann (von denen einige nicht einmal Typen enthalten Plugin) ist es auch in einem separaten Anwendungsdomäne ausgeführt wird, die entladen werden subsequntly kann. Beachten Sie, dass diese Klasse auch inherits MarshalByRefObject für den oben beschriebenen Gründen. Da Instanzen Type nicht zwischen Anwendungsdomänen übergeben werden kann, wird in diesem Erkennungsprozess einen benutzerdefinierten Typ namens TypeLocator die Zeichenfolge Namen zu speichern und Montage jedes entdeckte Art zu benennen, die dann sicher in die Haupt applicatin Domäne zurück geführt werden kann.

/// <summary>
/// Safely identifies assemblies within a designated plugin directory that contain qualifying plugin types.
/// </summary>
internal class PluginFinder : MarshalByRefObject
{
    internal const string PluginPath = @"..\..\..\Plugins\Output";

    private readonly Type _pluginBaseType;

    /// <summary>
    /// Initializes a new instance of the <see cref="PluginFinder"/> class.
    /// </summary>
    public PluginFinder()
    {
        // For some reason, compile-time types are not reference equal to the corresponding types referenced
        // in each plugin assembly, so equality must be tested by loading types by name from the Interop assembly.
        var interopAssemblyFile = Path.GetFullPath(Path.Combine(PluginPath, typeof(PluginBase).Assembly.GetName().Name) + ".dll");
        var interopAssembly = Assembly.LoadFrom(interopAssemblyFile);
        _pluginBaseType = interopAssembly.GetType(typeof(PluginBase).FullName);
    }

    /// <summary>
    /// Returns the name and assembly name of qualifying plugin classes found in assemblies within the designated plugin directory.
    /// </summary>
    /// <returns>An <see cref="IEnumerable{TypeLocator}"/> that represents the qualifying plugin types.</returns>
    public static IEnumerable<TypeLocator> FindPlugins()
    {
        AppDomain domain = null;

        try
        {
            domain = AppDomain.CreateDomain("Discovery Domain");

            var finder = (PluginFinder)domain.CreateInstanceAndUnwrap(typeof(PluginFinder).Assembly.FullName, typeof(PluginFinder).FullName);
            return finder.Find();
        }
        finally
        {
            if (domain != null)
            {
                AppDomain.Unload(domain);
            }
        }
    }

    /// <summary>
    /// Surveys the configured plugin path and returns the the set of types that qualify as plugin classes.
    /// </summary>
    /// <remarks>
    /// Since this method loads assemblies, it must be called from within a dedicated application domain that is subsequently unloaded.
    /// </remarks>
    private IEnumerable<TypeLocator> Find()
    {
        var result = new List<TypeLocator>();

        foreach (var file in Directory.GetFiles(Path.GetFullPath(PluginPath), "*.dll"))
        {
            try
            {
                var assembly = Assembly.LoadFrom(file);

                foreach (var type in assembly.GetExportedTypes())
                {
                    if (!type.Equals(_pluginBaseType) &&
                        _pluginBaseType.IsAssignableFrom(type))
                    {
                        result.Add(new TypeLocator(assembly.FullName, type.FullName));
                    }
                }
            }
            catch (Exception e)
            {
                // Ignore DLLs that are not .NET assemblies.
            }
        }

        return result;
    }
}

/// <summary>
/// Encapsulates the assembly name and type name for a <see cref="Type"/> in a serializable format.
/// </summary>
[Serializable]
internal class TypeLocator
{
    /// <summary>
    /// Initializes a new instance of the <see cref="TypeLocator"/> class.
    /// </summary>
    /// <param name="assemblyName">The name of the assembly containing the target type.</param>
    /// <param name="typeName">The name of the target type.</param>
    public TypeLocator(
        string assemblyName,
        string typeName)
    {
        if (string.IsNullOrEmpty(assemblyName)) throw new ArgumentNullException("assemblyName");
        if (string.IsNullOrEmpty(typeName)) throw new ArgumentNullException("typeName");

        AssemblyName = assemblyName;
        TypeName = typeName;
    }

    /// <summary>
    /// Gets the name of the assembly containing the target type.
    /// </summary>
    public string AssemblyName { get; private set; }

    /// <summary>
    /// Gets the name of the target type.
    /// </summary>
    public string TypeName { get; private set; }
}

Die Interop-Assembly enthält die Basisklasse für Klassen, die Plugin-Funktionalität implementieren wird (beachten Sie, dass es ergibt sich auch aus MarshalByRefObject.

Diese Baugruppe definiert auch die IHost Schnittstelle, die Plugins rufen zurück in die Host-Anwendung ermöglicht.

/// <summary>
/// Defines the interface common to all untrusted plugins.
/// </summary>
public abstract class PluginBase : MarshalByRefObject
{
    public abstract void Initialize(IHost host);

    public abstract void SaySomething();

    public abstract void DoSomethingDangerous();

    public abstract void CallBackToHost();
}

/// <summary>
/// Defines the interface through which untrusted plugins automate the host.
/// </summary>
public interface IHost
{
    void SaySomething();
}

Schließlich wird jedes Plugin ergibt sich aus der Basisklasse in der Interop-Assembly und implementiert seine abstrakten Methoden definiert. Es kann Montag in jeder Plugin mehrere Vererbungsklassen sein, und es kann mehr Plug-Baugruppen sein.

public class Plugin : PluginBase
{
    private IHost _host;

    public override void Initialize(
        IHost host)
    {
        _host = host;
    }

    public override void SaySomething()
    {
        Console.WriteLine("This is a message issued by type: {0}", GetType().FullName);
    }

    public override void DoSomethingDangerous()
    {
        var x = File.ReadAllText(@"C:\Test.txt");
    }

    public override void CallBackToHost()
    {
        _host.SaySomething();           
    }
}

Wenn Sie Ihre 3rd-Party-Erweiterungen Last mit einem niedrigeren Sicherheitsprivilegien als der Rest Ihrer Anwendung benötigen, sollten Sie eine neue AppDomain erstellen, ein MEF Container für Ihre Erweiterungen in dieser Anwendungsdomäne erstellen, und ruft dann marshall aus Ihrer Anwendung zu den Objekten in der Sandbox-Anwendungsdomäne. Die Sandbox tritt in, wie Sie die Anwendungsdomäne erstellen, es hat nichts zu mit MEF.

Vielen Dank für die Lösung mit uns zu teilen. Ich möchte einen wichtigen Kommentar und sugestion machen.

Der Kommentar ist, dass Sie nicht zu 100% können Sandbox ein Plugin, indem sie es in einer anderen AppDomain vom Host geladen werden. Um herauszufinden, zu aktualisieren DoSomethingDangerous die folgende:

public override void DoSomethingDangerous()                               
{                               
    new Thread(new ThreadStart(() => File.ReadAllText(@"C:\Test.txt"))).Start();
}

Eine nicht behandelte Ausnahme von einem Kind Faden angehoben wird, kann die gesamte Anwendung zum Absturz bringen.

diese für Informationen über unhandle Ausnahmen.

Sie können auch diese beiden Blog-Einträge aus dem System.AddIn Team lesen, die erklären, dass 100% Isolation kann nur sein, wenn das Add-In in einem anderen Prozess ist. Sie haben auch ein Beispiel dafür, was jemand tun kann Benachrichtigungen von Add-In erhalten, die angehobenen Ausnahmen behandeln scheitern.

http://blogs.msdn.com/b/clraddins/archive/2007/05/01/using-appdomain-isolation-to-detect-add-in-failures-jesse-kaplan. aspx

http://blogs.msdn.com/b/clraddins/archive/2007/05/03/more-on-logging-unhandledexeptions-from-managed-add-ins-jesse-kaplan. aspx

Nun ist die sugestion, dass ich wollte, hat machen mit der PluginFinder.FindPlugins Methode zu tun. Statt jeden Kandidaten Montag in einem neuen AppDomain des Ladens, was auf seine Art und das Entladen der AppDomain, können Sie Mono.Cecil . Sie werden dann nicht irgendetwas davon zu tun haben.

Es ist so einfach wie:

AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);

foreach (TypeDefinition td in ad.MainModule.GetTypes())
{
    if (td.BaseType != null && td.BaseType.FullName == "MyNamespace.MyTypeName")
    {        
        return true;
    }
}

Es gibt wahrscheinlich noch bessere Möglichkeiten, dies mit Cecil zu tun, aber ich bin kein Experte Benutzer dieser Bibliothek.

Grüße,

Eine Alternative wäre, diese Bibliothek zu verwenden: https://processdomain.codeplex.com/ Es ermöglicht Ihnen, alle .NET-Code in Out-of-Process-AppDomain ausgeführt wird, die noch bessere Isolation bietet, als die akzeptierte Antwort. Natürlich braucht man ein richtiges Werkzeug für ihre Aufgabe zu wählen und in vielen Fällen des Ansatz in der akzeptierten Antwort gegeben ist alles, was benötigt wird.

Allerdings, wenn Ihr mit .net arbeiten Plugins, die Call-in native Bibliotheken, die instabil sein können (die Situation, die ich persönlich kam in) Sie wollen sie laufen nicht nur in einer separaten Anwendungsdomäne, aber auch in einem separaten Prozess. Ein nettes Feature dieser Bibliothek ist, dass es den Prozess automatisch neu gestartet, wenn ein Plugin stürzt.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top