Переопределить хост ссылок на одапа вебапи

Я использую WebAPI 2.2 и Microsoft.AspNet.OData 5.7.0 для создания службы OData, которая поддерживает подкачку страниц.

При размещении в производственной среде WebAPI живет на сервере, который не предоставляется извне, поэтому различные ответы, возвращаемые в ответе OData, такие как @odata.context а также @odata.nextLink указать внутренний IP-адрес, например http://192.168.X.X/<AccountName>/api/... и т.п.

Я был в состоянии изменить Request.ODataProperties().NextLink реализуя некоторую логику в каждом методе ODataController, чтобы заменить внутренний URL-адрес внешним URL-адресом, например https://account-name.domain.com/api/..., но это очень неудобно, и это только исправляет NextLinks.

Есть ли способ установить имя внешнего хоста во время настройки службы OData? Я видел собственность Request.ODataProperties().Path и задаюсь вопросом, возможно ли установить базовый путь на config.MapODataServiceRoute("odata", "odata", GetModel()); позвонить или в GetModel() реализация с использованием, например, ODataConventionModelBuilder?


ОБНОВЛЕНИЕ: лучшее решение, которое я придумал, состоит в том, чтобы создать BaseODataController который отменяет Initialize метод и проверяет, является ли Request.RequestUri.Host.StartsWith("beginning-of-known-internal-IP-address") а затем сделать RequestUri переписать так:

var externalAddress = ConfigClient.Get().ExternalAddress;  // e.g. https://account-name.domain.com
var account = ConfigClient.Get().Id;  // e.g. AccountName
var uriToReplace = new Uri(new Uri("http://" + Request.RequestUri.Host), account);
string originalUri = Request.RequestUri.AbsoluteUri;
Request.RequestUri = new Uri(Request.RequestUri.AbsoluteUri.Replace(uriToReplace.AbsoluteUri, externalAddress));
string newUri = Request.RequestUri.AbsoluteUri;
this.GetLogger().Info($"Request URI was rewritten from {originalUri} to {newUri}");

Это прекрасно исправляет @odata.nextLink URL для всех контроллеров, но по какой-то причине @odata.context URL все еще получают AccountName часть (например, https://account-name.domain.com/AccountName/api/odata/$ metadata # ControllerName), поэтому они все еще не работают.

4 ответа

Решение

Переписать RequestUri достаточно, чтобы повлиять @odata.nextLink значения, потому что код, который вычисляет следующую ссылку, зависит от RequestUri непосредственно. Другой @odata.xxx ссылки рассчитываются через UrlHelper, который как- то ссылается на путь из исходного URI запроса. (Следовательно AccountName вы видите в своем @odata.context ссылка на сайт. Я видел такое поведение в своем коде, но не смог отследить источник кешированного пути URI.)

Вместо того, чтобы переписать RequestUri мы можем решить эту проблему, создав CustomUrlHelper класс для перезаписи ссылок OData на лету. Новый GetNextPageLink метод будет обрабатывать @odata.nextLink переписывает, а Link Переопределение метода будет обрабатывать все другие изменения.

public class CustomUrlHelper : System.Web.Http.Routing.UrlHelper
{
    public CustomUrlHelper(HttpRequestMessage request) : base(request)
    { }

    // Change these strings to suit your specific needs.
    private static readonly string ODataRouteName = "ODataRoute"; // Must be the same as used in api config
    private static readonly string TargetPrefix = "http://localhost:8080/somePathPrefix"; 
    private static readonly int TargetPrefixLength = TargetPrefix.Length;
    private static readonly string ReplacementPrefix = "http://www.contoso.com"; // Do not end with slash

    // Helper method.
    protected string ReplaceTargetPrefix(string link)
    {
        if (link.StartsWith(TargetPrefix))
        {
            if (link.Length == TargetPrefixLength)
            {
                link = ReplacementPrefix;
            }
            else if (link[TargetPrefixLength] == '/')
            {
                link = ReplacementPrefix + link.Substring(TargetPrefixLength);
            }
        }

        return link;
    }

    public override string Link(string routeName, IDictionary<string, object> routeValues)
    {
        var link = base.Link(routeName, routeValues);

        if (routeName == ODataRouteName)
        {
            link = this.ReplaceTargetPrefix(link);
        }

        return link;
    }

    public Uri GetNextPageLink(int pageSize)
    {
        return new Uri(this.ReplaceTargetPrefix(this.Request.GetNextPageLink(pageSize).ToString()));
    }
}

Подключить CustomUrlHelper в Initialize метод базового класса контроллеров.

public abstract class BaseODataController : ODataController
{
    protected abstract int DefaultPageSize { get; }

    protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
    {
        base.Initialize(controllerContext);

        var helper = new CustomUrlHelper(controllerContext.Request);
        controllerContext.RequestContext.Url = helper;
        controllerContext.Request.ODataProperties().NextLink = helper.GetNextPageLink(this.DefaultPageSize);
    }

Обратите внимание, что размер страницы будет одинаковым для всех действий в данном классе контроллеров. Вы можете обойти это ограничение, перемещая назначение ODataProperties().NextLink к телу конкретного метода действия следующим образом:

var helper = this.RequestContext.Url as CustomUrlHelper;
this.Request.ODataProperties().NextLink = helper.GetNextPageLink(otherPageSize);

Ответ Ленхареста многообещающий, но я нашел улучшение его метода. Вместо использования UrlHelper я создал класс, производный от System.Net.Http.DelegatingHandler. Этот класс вставляется (первым) в конвейер обработки сообщений и, таким образом, имеет уязвимость при изменении входящего HttpRequestMessage. Это улучшение по сравнению с указанным выше решением, потому что помимо изменения URL-адресов для конкретного контроллера (как это делает UrlHelper, например, https://data.contoso.com/odata/MyController), он также изменяет URL-адрес, который отображается как xml:base в документе службы OData (например, https://data.contoso.com/odata).

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

Обработчик сообщений выглядит так:

    public class BehindProxyMessageHandler : DelegatingHandler
    {
        protected async override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var builder = new UriBuilder(request.RequestUri);
            var visibleHost = builder.Host;
            var visibleScheme = builder.Scheme;
            var visiblePort = builder.Port;

            if (request.Headers.Contains("X-Forwarded-Host"))
            {
                string[] forwardedHosts = request.Headers.GetValues("X-Forwarded-Host").First().Split(new char[] { ',' });
                visibleHost = forwardedHosts[0].Trim();
            }

            if (request.Headers.Contains("X-Forwarded-Proto"))
            {
                visibleScheme = request.Headers.GetValues("X-Forwarded-Proto").First();
            }

            if (request.Headers.Contains("X-Forwarded-Port"))
            {
                try
                {
                    visiblePort = int.Parse(request.Headers.GetValues("X-Forwarded-Port").First());
                }
                catch (Exception)
                { }
            }

            builder.Host = visibleHost;
            builder.Scheme = visibleScheme;
            builder.Port = visiblePort;

            request.RequestUri = builder.Uri;
            var response = await base.SendAsync(request, cancellationToken);
            return response;
        }
    }

Вы подключаете обработчик к WebApiConfig.cs:

    config.Routes.MapODataServiceRoute(
        routeName: "odata",
        routePrefix: "odata",
        model: builder.GetEdmModel(),
        pathHandler: new DefaultODataPathHandler(),
        routingConventions: ODataRoutingConventions.CreateDefault()
    );
    config.MessageHandlers.Insert(0, new BehindProxyMessageHandler());

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

public class MasqueradeHostFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var appConfig = context.HttpContext.RequestServices.GetService<AppConfig>();
        if (!string.IsNullOrEmpty(appConfig?.MasqueradeHost))
            context.HttpContext.Request.Host = new HostString(appConfig.MasqueradeHost);
    }
}

Примените фильтр к базовому классу контроллера.

[MasqueradeHostFilter]
public class AppODataController : ODataController
{
}

Результатом является красиво отформатированный вывод:

{ "@odata.context":"https://app.example.com/odata/$metadata" }

Всего два цента.

Существует другое решение, но оно переопределяет URL для всего контекста. Я хотел бы предложить следующее:

  1. Создайте промежуточное ПО owin и переопределите свойства Host и Scheme внутри
  2. Зарегистрируйте промежуточное ПО как первое

Вот пример промежуточного программного обеспечения

public class RewriteUrlMiddleware : OwinMiddleware
{
    public RewriteUrlMiddleware(OwinMiddleware next)
        : base(next)
    {
    }

    public override async Task Invoke(IOwinContext context)
    {
        context.Request.Host = new HostString(Settings.Default.ProxyHost);
        context.Request.Scheme = Settings.Default.ProxyScheme;
        await Next.Invoke(context);
    }
}

ProxyHost - это хост, который вы хотите иметь. Пример: test.com

ProxyScheme - это схема, которую вы хотите: Пример: https

Пример регистрации промежуточного программного обеспечения

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Use(typeof(RewriteUrlMiddleware));
        var config = new HttpConfiguration();
        WebApiConfig.Register(config);
        app.UseWebApi(config);
    }
}

Использование system.web.odata 6.0.0.0.

Задание свойства NextLink слишком рано проблематично. Каждый ответ будет содержать следующую ссылку. Конечно, последняя страница не должна содержать таких украшений.

http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html говорит:

URL-адреса, присутствующие в полезной нагрузке (будь то запрос или ответ), МОГУТ быть представлены как относительные URL-адреса.

Один способ, который, я надеюсь, сработает, это переопределить EnableQueryAttribute:

public class myEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        var result = base.ApplyQuery(queryable, queryOptions);
        var nextlink = queryOptions.Request.ODataProperties().NextLink;
        if (nextlink != null)
            queryOptions.Request.ODataProperties().NextLink = queryOptions.Request.RequestUri.MakeRelativeUri(nextlink);
        return result;
    }
}

ApplyQuery() где обнаружен "переполнение". Это в основном просит pagesize+1 строки и будут устанавливать NextLink если результирующий набор содержит более pagesize строк.

На данный момент это относительно легко переписать NextLink на относительный URL.

Недостатком является то, что теперь каждый метод odata должен быть украшен новым атрибутом myEnableQuery:

[myEnableQuery]
public async Task<IHttpActionResult> Get(ODataQueryOptions<TElement> options)
{
  ...
}

и другие URL-адреса, встроенные в других местах, остаются проблематичными. odata.context остается проблемой. Я хочу не играть с URL-адресом запроса, потому что не вижу, как это можно поддерживать с течением времени.

Ваш вопрос сводится к контролю корневого URI службы изнутри самой службы. Моей первой мыслью было найти крючок для форматеров мультимедийных типов, используемых для сериализации ответов. ODataMediaTypeFormatter.MessageWriterSettings.PayloadBaseUri а также ODataMediaTypeFormatter.MessageWriterSettings.ODataUri.ServiceRoot оба устанавливаемых свойства, которые предлагают решение. К несчастью, ODataMediaTypeFormatter сбрасывает эти свойства при каждом вызове WriteToStreamAsync,

Обходной путь не очевиден, но если вы покопаетесь в исходном коде, то в конце концов получите вызов IODataPathHandler.Link, Обработчик пути является точкой расширения OData, поэтому вы можете создать собственный обработчик пути, который всегда возвращает абсолютный URI, который начинается с корня службы, который вы желаете.

public class CustomPathHandler : DefaultODataPathHandler
{
    private const string ServiceRoot = "http://example.com/";

    public override string Link(ODataPath path)
    {
        return ServiceRoot + base.Link(path);
    }
}

И затем зарегистрируйте этот обработчик пути во время настройки сервиса.

// config is an instance of HttpConfiguration
config.MapODataServiceRoute(
    routeName: "ODataRoute",
    routePrefix: null,
    model: builder.GetEdmModel(),
    pathHandler: new CustomPathHandler(),
    routingConventions: ODataRoutingConventions.CreateDefault()
);
Другие вопросы по тегам