ApsNet.Core 中的通用 passthrough/forwarding 表单数据

Generic passthrough/forwarding of form data in ApsNet.Core

我正在尝试创建一个网络钩子来接收来自 Twilio phone 号码的消息。但是,我不仅需要一个 webhook 来修改数据并立即 return 一个结果给 Twilio,我需要这个 webhook 将 Twilio 的消息传递到内部 API,等待响应,然后 然后 return Twilio 的结果。

这是我想出的一些通用代码,我希望它能起作用。

public async Task<HttpResponseMessage> ReceiveAndForwardSms(HttpContent smsContent)
        {
            var client = new HttpClient();
            
            HttpResponseMessage response = await client.PostAsync(Environment.GetEnvironmentVariable("requestUriBase") + "/api/SmsHandler/PostSms", smsContent);
            
            return response;
        }

此代码的问题是 Twilio 在进入函数之前立即 returns 一个 415 错误代码 (Unsupported Media Type)。

当我尝试接受“正确类型”(Twilio.AspNet.Common.SmsRequest) 时,我无法将 SmsRequest 填回表单编码对象并通过 client.PostAsync() 发送它... 例如:

public async Task<HttpResponseMessage> ReceiveAndForwardSms([FromForm]SmsRequest smsRequest)
{
    var client = new HttpClient();
    var stringContent = new StringContent(smsRequest.ToString());
    
    HttpResponseMessage response = await client.PostAsync(Environment.GetEnvironmentVariable("requestUriBase") + "/api/SmsHandler/PostSms", stringContent);
    return response;
}
  1. 我能做些什么来“屏蔽”函数的接受类型或保持第一个函数的通用性吗?
  2. 如何将此 SmsRequest 推回到“表单编码”对象中,以便我可以在我的消费服务中以同样的方式接受它?

TLDR 您的选择是:

  • 使用现有的反向代理,如 NGINX、HAProxy、F5
  • 使用 YARP 将反向代理功能添加到 ASP.NET 核心项目
  • 在控制器中接受 webhook 请求,将 headers 和数据映射到新的 HttpRequestMessage 并将其发送到您的私人服务,然后将您的私人服务的响应映射到响应回到 Twilio。

听起来您要构建的是 reverse proxy。在 Web 应用程序前面放置一个反向代理是很常见的,用于 SSL 终止、缓存、基于主机名或 URL 的路由等。 反向代理将接收 Twilio HTTP 请求,然后将其转发到正确的私有服务。专用服务响应反向代理转发回 Twilio。

我建议使用现有的反向代理而不是自己构建此功能。如果你真的想自己构建它,这里有一个我能够开始工作的示例:

在您的反向代理项目中,添加一个控制器:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;

namespace ReverseProxy.Controllers;

public class SmsController : Controller
{
    private static readonly HttpClient HttpClient;
    private readonly ILogger<SmsController> logger;
    private readonly string twilioWebhookServiceUrl;

    static SmsController()
    {
        // don't do this in production!
        var insecureHttpClientHandler = new HttpClientHandler();
        insecureHttpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true;
        HttpClient = new HttpClient(insecureHttpClientHandler);
    }

    public SmsController(ILogger<SmsController> logger, IConfiguration configuration)
    {
        this.logger = logger;
        twilioWebhookServiceUrl = configuration["TwilioWebhookServiceUrl"];
    }

    public async Task Index()
    {
        using var serviceRequest = new HttpRequestMessage(HttpMethod.Post, twilioWebhookServiceUrl);
        foreach (var header in Request.Headers)
        {
            serviceRequest.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
        }
        
        serviceRequest.Content = new FormUrlEncodedContent(
            Request.Form.ToDictionary(
                kv => kv.Key,
                kv => kv.Value.ToString()
            )
        );
        
        var serviceResponse = await HttpClient.SendAsync(serviceRequest);

        Response.ContentType = "application/xml";
        var headersDenyList = new HashSet<string>()
        {
            "Content-Length",
            "Date",
            "Transfer-Encoding"
        };
        foreach (var header in serviceResponse.Headers)
        {
            if(headersDenyList.Contains(header.Key)) continue;
            logger.LogInformation("Header: {Header}, Value: {Value}", header.Key, string.Join(',', header.Value));
            Response.Headers.Add(header.Key, new StringValues(header.Value.ToArray()));
        }

        await serviceResponse.Content.CopyToAsync(Response.Body);
    }
}

这将接受 Twilio webhook 请求,并将所有 headers 和内容转发到私有 Web 服务。请注意,即使我能够破解它直到它起作用,它可能不安全且性能不佳。您可能需要做更多的工作才能使其成为生产级代码。使用风险自负。

在你的私人服务的ASP.NET核心项目中,使用TwilioController接受请求:

using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Common;
using Twilio.AspNet.Core;
using Twilio.TwiML;

namespace Service.Controllers;

public class SmsController : TwilioController
{
    private readonly ILogger<SmsController> logger;

    public SmsController(ILogger<SmsController> logger)
    {
        this.logger = logger;
    }

    public IActionResult Index(SmsRequest smsRequest)
    {
        logger.LogInformation("SMS Received: {SmsId}", smsRequest.SmsSid);
        var response = new MessagingResponse();
        response.Message($"You sent: {smsRequest.Body}");
        return TwiML(response);
    }
}

与其使用反向代理控制器中的脆弱代码代理请求,我建议在您的反向代理项目中安装 YARP,这是一个 ASP.NET 基于核心的反向代理库。

dotnet add package Yarp.ReverseProxy

然后在appsettings.json中添加如下配置:

{
  ...
  "ReverseProxy": {
    "Routes": {
      "SmsRoute" : {
        "ClusterId": "SmsCluster",
        "Match": {
          "Path": "/sms"
        }
      }
    },
    "Clusters": {
      "SmsCluster": {
        "Destinations": {
          "SmsService1": {
            "Address": "https://localhost:7196"
          }
        }
      }
    }
  }
}

此配置会将任何对路径 /Sms 的请求转发给您的私人 ASP.NET 核心服务,在我的本地机器上是 运行ning在 https://localhost:7196.

您还需要更新 Program.cs 文件以开始使用 YARP:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();

当你现在 运行 这两个项目时,对 /sms 的 Twilio webhook 请求被转发到你的私人服务,你的私人服务将响应,你的反向代理服务会将响应转发回 Twilio。

使用 YARP,您可以通过配置甚至以编程方式做更多的事情,所以如果您有兴趣,我会查看 YARP docs.

如果您已经拥有像 NGINX、HAProxy、F5 等反向代理,配置它来转发您的请求可能比使用 YARP 更容易。

PS:这里是the source code for the hacky and YARP solution