ASP.NET core 2 act as reverse proxy usering rewrite middleware

时间:2018-09-18 20:05:29

标签: asp.net asp.net-mvc razor asp.net-core

I'm struggling to make my asp.net core 2 app act like a reverse proxy using URL Rewrite rules.

I have the following in my startup.cs:

var rewriteRules = new RewriteOptions()
                .AddRedirectToHttps();
                .AddRewrite(@"^POC/(.*)", "http://192.168.7.73:3001/$1", true);
app.UseRewriter(rewriteRules);

The rewrite rule is exactly as it is in my IIS settings (which I'm trying to replace with this method) which works fine.

I'm assuming it has something to do with forwarding the headers maybe? Or maybe I just don't understand how the Rewrite Middleware is supposed to work, if you want the requests to be forwarded instead of just rewritten relative to current hostname.

1 个答案:

答案 0 :(得分:6)

反向代理可以在中间件中仿真/实现:

首先,我们在启动类中添加IUrlRewriter服务和ProxyMiddleware。

public class Startup
{
    private readonly IConfiguration _configuration;

    public Startup(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IUrlRewriter>(new SingleRegexRewriter(@"^/POC/(.*)", "http://192.168.7.73:3001/$1"));
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseRewriter(new RewriteOptions().AddRedirectToHttps());
        app.UseMiddleware<ProxyMiddleware>();
    }
}

接下来,我们将创建IUrlRewriter的基本实现。 RewriteUri方法必须将HttpContext转换为绝对Uri。如果网址不应在中间件中重定向,则为null。

public interface IUrlRewriter
{
    Task<Uri> RewriteUri(HttpContext context);
}

public class SingleRegexRewriter : IUrlRewriter
{
    private readonly string _pattern;
    private readonly string _replacement;
    private readonly RegexOptions _options;

    public SingleRegexRewriter(string pattern, string replacement)
        : this(pattern, replacement, RegexOptions.None) { }

    public SingleRegexRewriter(string pattern, string replacement, RegexOptions options)
    {
        _pattern = pattern ?? throw new ArgumentNullException(nameof(pattern));
        _replacement = replacement ?? throw new ArgumentNullException(nameof(pattern));
        _options = options;
    }

    public Task<Uri> RewriteUri(HttpContext context)
    {
        string url = context.Request.Path + context.Request.QueryString;
        var newUri = Regex.Replace(url, _pattern, _replacement);

        if (Uri.TryCreate(newUri, UriKind.Absolute, out var targetUri))
        {
            return Task.FromResult(targetUri);
        }

        return Task.FromResult((Uri)null);
    }
}

然后是中间件(从aspnet代理repo的旧版本中窃取)并进行了定制。它将IUrlRewrite服务作为Invoke方法的参数。

管道是:

  • 尝试重写网址
  • 创建HttpRequestMessage
  • 复制请求标题和内容
  • 发送请求
  • 复制响应头
  • 复制回复内容
  • 完成

瞧瞧

public class ProxyMiddleware
{
    private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler()
    {
        AllowAutoRedirect = false,
        MaxConnectionsPerServer = int.MaxValue,
        UseCookies = false,
    });

    private const string CDN_HEADER_NAME = "Cache-Control";
    private static readonly string[] NotForwardedHttpHeaders = new[] { "Connection", "Host" };

    private readonly RequestDelegate _next;
    private readonly ILogger<ProxyMiddleware> _logger;

    public ProxyMiddleware(
           RequestDelegate next,
           ILogger<ProxyMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context, IUrlRewriter urlRewriter)
    {
        var targetUri = await urlRewriter.RewriteUri(context);

        if (targetUri != null)
        {
            var requestMessage = GenerateProxifiedRequest(context, targetUri);
            await SendAsync(context, requestMessage);

            return;
        }

        await _next(context);
    }

    private async Task SendAsync(HttpContext context, HttpRequestMessage requestMessage)
    {
        using (var responseMessage = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted))
        {
            context.Response.StatusCode = (int)responseMessage.StatusCode;

            foreach (var header in responseMessage.Headers)
            {
                context.Response.Headers[header.Key] = header.Value.ToArray();
            }

            foreach (var header in responseMessage.Content.Headers)
            {
                context.Response.Headers[header.Key] = header.Value.ToArray();
            }

            context.Response.Headers.Remove("transfer-encoding");

            if (!context.Response.Headers.ContainsKey(CDN_HEADER_NAME))
            {
                context.Response.Headers.Add(CDN_HEADER_NAME, "no-cache, no-store");
            }

            await responseMessage.Content.CopyToAsync(context.Response.Body);
        }
    }

    private static HttpRequestMessage GenerateProxifiedRequest(HttpContext context, Uri targetUri)
    {
        var requestMessage = new HttpRequestMessage();
        CopyRequestContentAndHeaders(context, requestMessage);

        requestMessage.RequestUri = targetUri;
        requestMessage.Headers.Host = targetUri.Host;
        requestMessage.Method = GetMethod(context.Request.Method);


        return requestMessage;
    }

    private static void CopyRequestContentAndHeaders(HttpContext context, HttpRequestMessage requestMessage)
    {
        var requestMethod = context.Request.Method;
        if (!HttpMethods.IsGet(requestMethod) &&
            !HttpMethods.IsHead(requestMethod) &&
            !HttpMethods.IsDelete(requestMethod) &&
            !HttpMethods.IsTrace(requestMethod))
        {
            var streamContent = new StreamContent(context.Request.Body);
            requestMessage.Content = streamContent;
        }

        foreach (var header in context.Request.Headers)
        {
            if (!NotForwardedHttpHeaders.Contains(header.Key))
            {
                if (header.Key != "User-Agent")
                {
                    if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
                    {
                        requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
                    }
                }
                else
                {
                    string userAgent = header.Value.Count > 0 ? (header.Value[0] + " " + context.TraceIdentifier) : string.Empty;

                    if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, userAgent) && requestMessage.Content != null)
                    {
                        requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, userAgent);
                    }
                }

            }
        }
    }

    private static HttpMethod GetMethod(string method)
    {
        if (HttpMethods.IsDelete(method)) return HttpMethod.Delete;
        if (HttpMethods.IsGet(method)) return HttpMethod.Get;
        if (HttpMethods.IsHead(method)) return HttpMethod.Head;
        if (HttpMethods.IsOptions(method)) return HttpMethod.Options;
        if (HttpMethods.IsPost(method)) return HttpMethod.Post;
        if (HttpMethods.IsPut(method)) return HttpMethod.Put;
        if (HttpMethods.IsTrace(method)) return HttpMethod.Trace;
        return new HttpMethod(method);
    }
}

奖金:其他重写器

public class PrefixRewriter : IUrlRewriter
{
    private readonly PathString _prefix;
    private readonly string _newHost;

    public PrefixRewriter(PathString prefix, string newHost)
    {
        _prefix = prefix;
        _newHost = newHost;
    }

    public Task<Uri> RewriteUri(HttpContext context)
    {
        if (context.Request.Path.StartsWithSegments(_prefix))
        {
            var newUri = context.Request.Path.Value.Remove(0, _prefix.Value.Length) + context.Request.QueryString;
            var targetUri = new Uri(_newHost + newUri);
            return Task.FromResult(targetUri);
        }

        return Task.FromResult((Uri)null);
    }
}

public class MergeRewriter : IUrlRewriter
{
    private readonly List<IUrlRewriter> _rewriters = new List<IUrlRewriter>();
    public MergeRewriter()
    {
    }
    public MergeRewriter(IEnumerable<IUrlRewriter> rewriters)
    {
        if (rewriters == null) throw new ArgumentNullException(nameof(rewriters));

        _rewriters.AddRange(rewriters);
    }

    public MergeRewriter Add(IUrlRewriter rewriter)
    {
        if (rewriter == null) throw new ArgumentNullException(nameof(rewriter));

        _rewriters.Add(rewriter);

        return this;
    }

    public async Task<Uri> RewriteUri(HttpContext context)
    {
        foreach (var rewriter in _rewriters)
        {
            var targetUri = await rewriter.RewriteUri(context);
            if(targetUri != null)
            {
                return targetUri;
            }
        }

        return null;
    }
}

// In Statup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IUrlRewriter>(new MergeRewriter()
        .Add(new PrefixRewriter("/POC/API", "http://localhost:1234"))
        .Add(new SingleRegexRewriter(@"^/POC/(.*)", "http://192.168.7.73:3001/$1")));
}

编辑

我发现一个项目可以做同样的事情,但是还有更多其他功能https://github.com/damianh/ProxyKit作为nuget包