Вопрос

Я ищу простой и безопасный способ доступа к плагинам из приложения .Net. Хотя я представляю, что это очень распространенное требование, я изо всех сил пытаюсь найти все, что отвечает всем моим потребностям:

  • Приложение Host обнаружит и загружает свои сборки плагинов во время выполнения
  • Плагины будут созданы неизвестными 3-х сторонами, поэтому они должны быть песочницы, чтобы предотвратить их выполнение вредоносного кода
  • Обычный вспомогательный агрегат будет содержать типы, которые ссылаются как хост, так и его плагины
  • Каждый сборщик плагина будет содержать один или несколько классов, которые реализуют общий интерфейс плагина
  • При инициализации экземпляра плагина хост пройдет его ссылку на себя в виде интерфейса хоста
  • Хост будет звонить в плагин через свой общий интерфейс, и плагины могут обращаться к хосту аналогичным образом
  • Хост и плагины обмениваются данными в виде типов, определенных в сборке Interop (включая общие типы)

Я исследовал как MEF, так и MAF, но я изо всех сил пытаюсь увидеть, как любой из них может быть сделан, чтобы соответствовать законопроекту.

Предполагая, что мое понимание правильно, MAF не может поддерживать прохождение общих типов на границе изоляции, что важно для моего приложения. (MAF также очень сложен для реализации, но я был бы готов работать с этим, если бы я мог решить проблему общего типа).

MEF - это почти идеальное решение, но, по-видимому, не хватает требований безопасности, поскольку он загружает свои удлинительные сборки в том же AppDomain, что и хост, и, следовательно,, по-видимому, предотвращает песочницу.

Я видел этот вопрос, который разговаривает о запуске МЭФ в песочнительном режиме, но не описывает, как. Эта почта Указывает, что «при использовании MEF вы должны доверять расширениям не управлять вредоносным кодом или предлагать защиту через безопасность доступа кода», но, опять же, он не описывает, как. Наконец, есть эта почта, который описывает, как предотвратить нагрузку неизвестных плагинов, но это не подходит для моей ситуации, так как даже законные плагины будут неизвестны.

Мне удалось подать заявку на применение атрибутов безопасности .NE. такие как методы System.IO.File) помечены как SecuritySafeCritical, что означает, что они доступны из SecurityTransparent сборки. Я что-то упускаю здесь? Есть ли несколько дополнений, который я могу предпринять, чтобы сказать MEF, что он должен предоставить интернет-привилегии к узлам плагинов?

Наконец, я также смотрел на создание собственной простой архитектуры плагинов с песочницей, используя отдельный Appdomain, как описано здесь. Отказ Однако, насколько я вижу, эта техника позволяет мне использовать поздние привязки, чтобы вызвать статические методы на классах в ненадежной сборке. Когда я пытаюсь расширить этот подход к созданию экземпляра одного из моих классов плагинов, возвращенный экземпляр нельзя бросить на общий интерфейс плагина, что означает, что для приложения Host невозможно позвонить в него. Есть ли некоторый техника, которую я могу использовать, чтобы получить сильно напечатанный доход прокси по границе Appdomain?

Я прошу прощения за длину этого вопроса; Причина заключалась в том, чтобы показать все проспекты, которые я уже исследовал, в надежде, что кто-то может предложить что-то новое, чтобы попробовать.

Большое спасибо за ваши идеи, Тим

Это было полезно?

Решение

Потому что вы в разных empdomains, вы не можете просто передать экземпляр.

Вам нужно будет сделать удаленные плагины и создать прокси в основном приложении. Посмотрите на документы для Createinstanceandunwrap, который имеет пример того, как все это может работать на дно.

Это также еще один гораздо более широкий Обзор Джона Шемиц Что я думаю, хорошо прочитать. Удачи.

Другие советы

Я принял ответ Alastair MAW, так как это было его предложение и ссылки, которые привели меня к работочному решению, но я размещаю здесь некоторые детали именно то, что я сделал, для кого-то еще, кто пытается достичь чего-то подобного.

Как напоминание, в самой простой форме мое приложение содержит три сборки:

  • Основная сборка приложений, которая потребляет плагины
  • Сборка Interop, которая определяет общие типы, совместно используемые приложением и его плагинами
  • Образец плагина сборки

Ниже приведен упрощенную версию моего реального кода, показывая только то, что требуется для обнаружения и плагинов нагрузки, каждый по себе AppDomain:

Начиная с основной сборки приложения, главный класс программы использует утилитую класс с именем PluginFinder Чтобы открыть квалификационные типы плагинов в любых узлах в обозначенной папке плагинов. Для каждого из этих типов он создает экземпляр SANDOX AppDomain (С разрешениями в интернет-зоне) и использует его для создания экземпляра обнаруженного типа плагина.

При создании АН AppDomain С ограниченными разрешениями можно указать одну или несколько доверенных собраний, которые не подлежат этим разрешениям. Для достижения этого в сценарии, представленном здесь, основной сборки приложения и его зависимости (ассамблея взаимодействия) должны быть подписаны.

Для каждого нагруженного экземпляра плагина пользовательские методы в плагине можно назвать через его известной интерфейс, и плагин также может перезвонить обратно к приложению хоста через свой известный интерфейс. Наконец, приложение Host не загружает каждую из доменов песочницы.

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);
    }
}

В этом примерном коде класс приложения хоста очень прост, выставляя только один метод, который может быть вызван плагинами. Тем не менее, этот класс должен получить из MarshalByRefObject Так что его можно ссылаться между доменами приложений.

/// <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");
    }
}

То PluginFinder Класс имеет только один общественный метод, который возвращает список обнаруженных типов плагинов. Этот процесс обнаружения загружает каждую сборку, которую он находит и использует отражение для определения его квалификационных типов. Поскольку этот процесс может потенциально загружать много узлов (некоторые из которых даже не содержат типов плагинов), оно также выполняется в отдельном домене приложений, что может быть подразумено. Обратите внимание, что этот класс также наследует MarshalByRefObject по причинам, описанным выше. Поскольку экземпляры Type Не может быть передано между доменами приложений, этот процесс обнаружения использует пользовательский тип называемый TypeLocator Чтобы сохранить имя строки и имя сборки каждого обнаруженного типа, которое затем может быть безопасно передано обратно в основной домен приложения.

/// <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; }
}

Сборка Interop содержит базовый класс для классов, которые будут реализовывать функциональность плагина (обратите внимание, что она также происходит от MarshalByRefObject.

Эта сборка также определяет IHost Интерфейс, который позволяет плагину перезвонить в приложение хоста.

/// <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();
}

Наконец, каждый плагин получает от базового класса, определенного в сборке Interop, и реализует его абстрактные методы. Там могут быть несколько наследующих классов в любой сборке плагина, и могут быть несколько узлов плагинов.

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();           
    }
}

Если вам нужны дополнительные расширения для загрузки с более низкими привилегиями безопасности, чем остальное ваше приложение, вы должны создать новый AppDomain, создайте меф-контейнер для ваших расширений в этом домене приложения, а затем Marshall вызовы от вашего приложения к объектам в домене приложения с песочкой. Песочница возникает в том, как вы создаете домен приложения, он не имеет ничего общего с MEF.

Спасибо, что поделились с нами решение. Я хотел бы сделать важный комментарий и последовательность.

Комментарий состоит в том, что вы не можете 100% песочницу плагин, загрузив его в другое приложение от хоста. Чтобы выяснить, обновить досеть от следующих действий:

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

Необходимое необработанное исключение, поднятое дочерней нитью, может выровнять все приложение.

Читать это Для получения информации о неразделенных исключениях.

Вы также можете прочитать эти два записей блога из команды System.Addin, которые объясняют, что 100% изоляция может быть только тогда, когда надстройка находится в другом процессе. У них также есть пример того, что кто-то может сделать, чтобы получить уведомления от добавлений, которые не могут обрабатывать поднятые исключения.

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-unhandledexpeptions-from-manage-add-ins-jesse-kaplan.aspx.

Теперь последователь, который я хотел сделать, связан с методом плагинфинфдер. Вместо того, чтобы загрузка каждого кандидата в сборе в новом Appdomain, отражая его типы и разгрузить приложение, вы можете использовать Mono.cecil.. Отказ Вы тогда не придется делать ни одно из этого.

Это так просто, как:

AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);

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

Вероятно, есть еще лучшие способы сделать это с Cecil, но я не являюсь экспертом этой библиотеки.

С уважением,

Альтернативой будет использовать эту библиотеку: https://processdomain.codeplex.com/Это позволяет вам запустить любой .NET код в Out-Process Appdomain, который обеспечивает еще лучшую изоляцию, чем принятый ответ. Конечно, нужно выбрать правильный инструмент для их задачи, и во многих случаях подход, приведенный в принятом ответе, - это все, что необходимо.

Однако, если вы работаете с плагинами .NET, которые звонят в родные библиотеки, которые могут быть нестабильными (ситуация, которую я лично вошел) вы хочу Чтобы запустить их не только в отдельном домене приложений, но и в отдельном процессе. Приятной особенностью этой библиотеки является то, что она автоматически перезапустится процесс, если плагин сбивает его.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top