Подтвердить что ты не робот

Как перехватить 404 с использованием промежуточного ПО Owin

Фон

Сначала позвольте мне объяснить предысторию. Я работаю над проектом, который пытается выйти замуж за бэкэнд-сервер, который использует Web API, настроенный через OWIN, размещенный в IIS, но потенциально другие поддерживаемые OWIN хосты в будущем - интерфейс с использованием AngularJS.

Интерфейс AngularJS полностью статичный. Я полностью исключаю серверные технологии, такие как MVC/Razor, WebForms, Bundles, все, что связано с интерфейсом и используемыми им ресурсами, и откладывать вместо этого самые последние и самые лучшие методы, используя Node.js, Grunt/Gulp и т.д., чтобы обрабатывать компиляцию CSS, связывание, минимизацию и т.д. По причинам, в которые я не буду входить, я держу проекты интерфейса и сервера в разных местах в рамках одного и того же проекта (вместо того, чтобы вставлять их в проект Host непосредственно (см. диаграмму ниже).

MyProject.sln
server
  MyProject.Host
     MyProject.Host.csproj
     Startup.cs
     (etc.)
frontend
  MyProjectApp
     app.js
     index.html
     MyProjectApp.njproj
     (etc.)

Итак, что касается внешнего интерфейса, все, что мне нужно сделать, это заставить мой хост обслуживать мой статический контент. В Express.js это тривиально. С OWIN я смог сделать это легко, используя Microsoft.Owin.StaticFiles промежуточное программное обеспечение, и он отлично работает (очень сильно).

Вот моя конфигурация OwinStartup:

string dir = AppDomain.CurrentDomain.RelativeSearchPath; // get executing path
string contentPath = Path.GetFullPath(Path.Combine(dir, @"../../../frontend/MyProjectApp")); // resolve nearby frontend project directory

app.UseFileServer(new FileServerOptions
{
    EnableDefaultFiles = true,
    FileSystem = new PhysicalFileSystem(contentPath),
    RequestPath = new PathString(string.Empty) // starts at the root of the host
});

// ensure the above occur before map handler to prevent native static content handler
app.UseStageMarker(PipelineStage.MapHandler);

Поймать

В принципе, он просто размещает все в frontend/MyProjectApp, как если бы он находился прямо в корне MyProject.Host. Поэтому, естественно, если вы запрашиваете файл, который не существует, IIS генерирует ошибку 404.

Теперь, поскольку это приложение AngularJS и поддерживает html5mode, у меня будут некоторые маршруты, которые не являются физическими файлами на сервере, но обрабатываются как маршруты в приложении AngularJS. Если пользователь должен был уйти на AngularJS (что-то кроме index.html или файл, который физически существует, в этом примере), я бы получил 404, хотя этот маршрут может быть действительным в приложении AngularJS. Поэтому мне нужно мое промежуточное ПО OWIN, чтобы вернуть файл index.html в случае, если запрошенный файл не существует, и пусть мое приложение AngularJS выяснит, действительно ли это 404.

Если вы знакомы с SPA и AngularJS, это обычный и прямой подход. Если бы я использовал MVC или ASP.NET-маршрутизацию, я мог бы просто установить маршрут по умолчанию на контроллер MVC, который возвращает мой index.html, или что-то в этом направлении. Тем не менее, я уже заявил, что не использую MVC, и я стараюсь, чтобы это было максимально простым и легким.

Этот пользователь имел аналогичную дилемму и решил его с переписыванием IIS. В моем случае это не работает, потому что: а) мой контент физически не существует, где модуль URL перезаписи может его найти, поэтому он всегда возвращает index.html и b) Я хочу что-то, что не полагается на IIS, но обрабатывается в промежуточном ПО OWIN, поэтому его можно использовать гибко.

TL; DNR me, для громкого крика.

Просто, как я могу перехватить 404 Not Found и вернуть содержимое (примечание: не перенаправить) my FileServer -served index.html с использованием промежуточного программного обеспечения OWIN?

4b9b3361

Ответ 1

Я написал этот небольшой компонент промежуточного программного обеспечения, но я не знаю, слишком ли он переполнен, неэффективен или есть другие подводные камни. В основном это просто использует те же самые FileServerOptions FileServerMiddleware, самая важная часть - FileSystem, которую мы используем. Он помещается перед вышеупомянутым промежуточным программным обеспечением и выполняет быструю проверку, чтобы узнать, существует ли запрошенный путь. Если нет, путь запроса будет переписан как "index.html", и обычная StaticFileMiddleware будет взята оттуда.

Очевидно, что его можно было бы очистить для повторного использования, в том числе способ определить разные файлы по умолчанию для разных корневых путей (например, что-либо, запрашиваемое из "/feature1", которое отсутствует, должно использовать "/feature1/index.html", аналогично с "/feature2" и "/feature2/default.html" и т.д.).

Но пока это работает для меня. Очевидно, что это зависит от Microsoft.Owin.StaticFiles.

public class DefaultFileRewriterMiddleware : OwinMiddleware
{
    private readonly FileServerOptions _options;

    /// <summary>
    /// Instantiates the middleware with an optional pointer to the next component.
    /// </summary>
    /// <param name="next"/>
    /// <param name="options"></param>
    public DefaultFileRewriterMiddleware(OwinMiddleware next, FileServerOptions options) : base(next)
    {
        _options = options;
    }

    #region Overrides of OwinMiddleware

    /// <summary>
    /// Process an individual request.
    /// </summary>
    /// <param name="context"/>
    /// <returns/>
    public override async Task Invoke(IOwinContext context)
    {
        IFileInfo fileInfo;
        PathString subpath;

        if (!TryMatchPath(context, _options.RequestPath, false, out subpath) ||
            !_options.FileSystem.TryGetFileInfo(subpath.Value, out fileInfo))
        {
            context.Request.Path = new PathString(_options.RequestPath + "/index.html");
        }

        await Next.Invoke(context);
    }

    #endregion

    internal static bool PathEndsInSlash(PathString path)
    {
        return path.Value.EndsWith("/", StringComparison.Ordinal);
    }

    internal static bool TryMatchPath(IOwinContext context, PathString matchUrl, bool forDirectory, out PathString subpath)
    {
        var path = context.Request.Path;

        if (forDirectory && !PathEndsInSlash(path))
        {
            path += new PathString("/");
        }

        if (path.StartsWithSegments(matchUrl, out subpath))
        {
            return true;
        }
        return false;
    }
}

Ответ 2

Если вы используете OWIN, вы сможете использовать это:

using AppFunc = Func<
       IDictionary<string, object>, // Environment
       Task>; // Done

public static class AngularServerExtension
{
    public static IAppBuilder UseAngularServer(this IAppBuilder builder, string rootPath, string entryPath)
    {
        var options = new AngularServerOptions()
        {
            FileServerOptions = new FileServerOptions()
            {
                EnableDirectoryBrowsing = false,
                FileSystem = new PhysicalFileSystem(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath))
            },
            EntryPath = new PathString(entryPath)
        };

        builder.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions);

        return builder.Use(new Func<AppFunc, AppFunc>(next => new AngularServerMiddleware(next, options).Invoke));    
    }
}

public class AngularServerOptions
{
    public FileServerOptions FileServerOptions { get; set; }

    public PathString EntryPath { get; set; }

    public bool Html5Mode
    {
        get
        {
            return EntryPath.HasValue;
        }
    }

    public AngularServerOptions()
    {
        FileServerOptions = new FileServerOptions();
        EntryPath = PathString.Empty;
    }
}

public class AngularServerMiddleware
{
    private readonly AngularServerOptions _options;
    private readonly AppFunc _next;
    private readonly StaticFileMiddleware _innerMiddleware;

    public AngularServerMiddleware(AppFunc next, AngularServerOptions options)
    {
        _next = next;
        _options = options;

        _innerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions);
    }

    public async Task Invoke(IDictionary<string, object> arg)
    {
        await _innerMiddleware.Invoke(arg);
        // route to root path if the status code is 404
        // and need support angular html5mode
        if ((int)arg["owin.ResponseStatusCode"] == 404 && _options.Html5Mode)
        {
            arg["owin.RequestPath"] = _options.EntryPath.Value;
            await _innerMiddleware.Invoke(arg);
        }
    }
}

Ответ 3

Решение, которое предоставил Хавьер Фигероа, действительно работает для моего проекта. Задняя часть моей программы является автономным веб-сервером OWIN, и я использую AngularJS с включенным html5Mode в качестве интерфейса. Я пробовал много разных способов писать связующее ПО IOwinContext, и никто из них не работает до тех пор, пока я его не найду, он, наконец, работает! Спасибо, что поделились этим решением.

решение, предоставленное Хавьер Фигероа

Кстати, следующим образом я применил этот AngularServerExtension в моем классе запуска OWIN:

        // declare the use of UseAngularServer extention
        // "/" <= the rootPath
        // "/index.html" <= the entryPath
        app.UseAngularServer("/", "/index.html");

        // Setting OWIN based web root directory
        app.UseFileServer(new FileServerOptions()
        {
            RequestPath = PathString.Empty,
            FileSystem = new PhysicalFileSystem(@staticFilesDir), // point to the root directory of my web server
        });

Ответ 4

Ответ, данный Хавьер Фигероа здесь работает и очень полезен! Спасибо за это! Однако он имеет одно странное поведение: всякий раз, когда ничего не существует (включая файл записи), он дважды запускает конвейер next. Например, ниже тест терпит неудачу, когда я применяю эту реализацию через UseHtml5Mode:

[Test]
public async Task ShouldRunNextMiddlewareOnceWhenNothingExists()
{
    // ARRANGE
    int hitCount = 0;
    var server = TestServer.Create(app =>
    {
        app.UseHtml5Mode("test-resources", "/does-not-exist.html");
        app.UseCountingMiddleware(() => { hitCount++; });
    });

    using (server)
    {
        // ACT
        await server.HttpClient.GetAsync("/does-not-exist.html");

        // ASSERT
        Assert.AreEqual(1, hitCount);
    }
}

Несколько заметок о моем вышеописанном тесте, если кто-то интересуется:

Реализация, с которой я пошел, которая делает вышеуказанный тестовый проход, выглядит следующим образом:

namespace Foo 
{
    using AppFunc = Func<IDictionary<string, object>, Task>;

    public class Html5ModeMiddleware
    {
        private readonly Html5ModeOptions m_Options;
        private readonly StaticFileMiddleware m_InnerMiddleware;
        private readonly StaticFileMiddleware m_EntryPointAwareInnerMiddleware;

        public Html5ModeMiddleware(AppFunc next, Html5ModeOptions options)
        {
            if (next == null) throw new ArgumentNullException(nameof(next));
            if (options == null) throw new ArgumentNullException(nameof(options));

            m_Options = options;
            m_InnerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions);
            m_EntryPointAwareInnerMiddleware = new StaticFileMiddleware((environment) =>
            {
                var context = new OwinContext(environment);
                context.Request.Path = m_Options.EntryPath;
                return m_InnerMiddleware.Invoke(environment);

            }, options.FileServerOptions.StaticFileOptions);
        }

        public Task Invoke(IDictionary<string, object> environment) => 
            m_EntryPointAwareInnerMiddleware.Invoke(environment);
    }
}

Расширение довольно похоже:

namespace Owin
{
    using AppFunc = Func<IDictionary<string, object>, Task>;

    public static class AppBuilderExtensions
    {
        public static IAppBuilder UseHtml5Mode(this IAppBuilder app, string rootPath, string entryPath)
        {
            if (app == null) throw new ArgumentNullException(nameof(app));
            if (rootPath == null) throw new ArgumentNullException(nameof(rootPath));
            if (entryPath == null) throw new ArgumentNullException(nameof(entryPath));

            var options = new Html5ModeOptions
            {
                EntryPath = new PathString(entryPath),
                FileServerOptions = new FileServerOptions()
                {
                    EnableDirectoryBrowsing = false,
                    FileSystem = new PhysicalFileSystem(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath))
                }
            };

            app.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions);

            return app.Use(new Func<AppFunc, AppFunc>(next => new Html5ModeMiddleware(next, options).Invoke));
        }
    }
}