Web API 2 encodes URLs to Google APIs - asp.net-web-api2

I am using Web API 2 to create a Google Analytics Reporting API v4 proxy. I am successfully creating the request parameters using the Google.Apis.AnalyticsReporting.v4 library and succesfully authenticating with a service account. However, when running the BatchGet.Execute() method, the request for batch reports fail.
Fiddler shows the request is a 404:
The requested URL /v4/reports%3AbatchGet was not found on this server. That’s all we know.
Using Postman I can see that the if the URL was correct (https://analyticsreporting.googleapis.com/v4/reports:batchGet) the request succeeds. Something on the back-end of Web API or .NET 4.6.1 is encoding the request URL and causing Google to send back a 404.
How can I configure Web API 2 so that it allows the colon within the URL for the Google API request?
public IHttpActionResult Post([FromBody] RequestObject req)
{
var resp = new CompleteResponse();
try
{
var service = new AnalyticsReportingService(new BaseClientService.Initializer
{
HttpClientInitializer = Cert,
ApplicationName = "GA Test"
});
var report = new GetReportsRequest();
var result = service.Reports.BatchGet(report).Execute();
resp.Result = result.Reports[0].Data;
}
catch (Exception ex)
{
resp.Error = ex.ToString();
}
return Ok(resp);
}
Global.asax:
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
GlobalConfiguration.Configuration.Formatters.JsonFormatter.MediaTypeMappings.Add(new System.Net.Http.Formatting.RequestHeaderMapping("Accept",
"text/html",
StringComparison.InvariantCultureIgnoreCase,
true,
"application/json"));
}
WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}

There is a known bug that was fixed in 1.31.1. The solution was updating to the latest version.
See https://github.com/google/google-api-dotnet-client/pull/1121

Related

Generic passthrough/forwarding of form data in ApsNet.Core

I'm attempting to create a webhook to receive messages from a Twilio phone number. But instead of just needing a webhook that will modify the data and immediately return a result to Twilio, I need this webhook to pass Twilio's message into an internal API, wait for the response, and then return the result to Twilio.
Here's some generic code I came up with that I hoped would work.
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;
}
The problem with this code is that Twilio immediately returns a 415 error code (Unsupported Media Type) before entering the function.
When I try to accept the "correct type" (Twilio.AspNet.Common.SmsRequest), I am unable to stuff the SmsRequest back into a form-encoded object and send it via client.PostAsync()...
Ex.:
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;
}
Is there anything I can do to "mask" the function's accepted type or keep this first function generic?
How do I go about shoving this SmsRequest back into a "form-encoded" object so I can accept it the same way in my consuming service?
TLDR
Your options are:
Use an existing reverse proxy like NGINX, HAProxy, F5
Use YARP to add reverse proxy functionality to an ASP.NET Core project
Accept the webhook request in a controller, map the headers and data to a new HttpRequestMessage and send it to your private service, then map the response of your private service, to the response back to Twilio.
It sounds like what you're trying to build is a reverse proxy. It is very common to put a reverse proxy in front of your web application for SSL termination, caching, routing based on hostname or URL, etc.
The reverse proxy will receive the Twilio HTTP request and then forwards it to the correct private service. The private service responds which the reverse proxy forwards back to Twilio.
I would recommend using an existing reverse proxy instead of building this functionality yourself. If you really want to build it yourself, here's a sample I was able to get working:
In your reverse proxy project, add a controller as such:
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);
}
}
This will accept the Twilio webhook request, and forward all headers and content to the private web service. Be warned, even though I was able to hack this together until it works, it is probably not secure and not performant. You'll probably have to do a lot more to get this to become production level code. Use at your own risk.
In the ASP.NET Core project for your private service, use a TwilioController to accept the request:
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);
}
}
Instead of proxying the request using the brittle code in the reverse proxy controller, I'd recommend installing YARP in your reverse proxy project, which is an ASP.NET Core based reverse proxy library.
dotnet add package Yarp.ReverseProxy
Then add the following configuration to appsettings.json:
{
...
"ReverseProxy": {
"Routes": {
"SmsRoute" : {
"ClusterId": "SmsCluster",
"Match": {
"Path": "/sms"
}
}
},
"Clusters": {
"SmsCluster": {
"Destinations": {
"SmsService1": {
"Address": "https://localhost:7196"
}
}
}
}
}
}
This configuration will forward any request to the path /Sms, to your private ASP.NET Core service, which on my local machine is running at https://localhost:7196.
You also need to update your Program.cs file to start using YARP:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
When you run both projects now, the Twilio webhook request to /sms is forwarded to your private service, your private service will respond, and your reverse proxy service will forward the response back to Twilio.
Using YARP you can do a lot more through configuration or even programmatically, so if you're interested I'd check out the YARP docs.
If you already have a reverse proxy like NGINX, HAProxy, F5, etc. it may be easier to configure that to forward your request instead of using YARP.
PS: Here's the source code for the hacky and YARP solution

Stripped header in request from client to server

Recently I have transferred my simple application which consists of angular 7 client and asp .net core web api (.net core v2.2) to production environment and I encountered a strange problem. When I tried to make an action which should do a post request with authentication token in header i got a 401 - Unauthorized server error.
My production environment is Ubuntu 18.04 with Apache2 server with proxy set to redirect from port 80 to port 5000 (this is the place where API is).
On development environment everything worked perfectly without any error so I guess that there is something with transferring request header from apache to kestrel.
I tried to google this problem and on Microsoft's website I found that I need to use forwarded headers method in my startup.cs file like this:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseAuthentication();
I configured my startup class like that but without any success.
Any help or direction where to try to find the solution would be appreciated.
Thank you very much in advance.
try to add a custome header throgh middleware.
namespace Middleware
{
public class CustomHeader
{
private readonly RequestDelegate _next;
public CustomHeader(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context,
{
var dictionary = new Dictionary<string, string[]>()
{
{
"Access-Control-Allow-Headers",new string[]{ "authorization" }
}
};
//To add Headers AFTER everything you need to do this
context.Response.OnStarting(state => {
var httpContext = (HttpContext)state;
foreach (var item in dictionary)
{
httpContext.Response.Headers.Add(item.Key, item.Value);
}
return Task.FromResult(0);
}, context);
await _next(context);
}
}
}

Authenticate a .NET Core 2.1 SignalR console client to a web app that uses "Identity as UI"

Using .NET Core 2.1 & VS2017 preview 2 I created a simple web server with "Identity as UI" as explained here and then added a SignalR chat following this.
In particular I have:
app.UseAuthentication();
app.UseSignalR((options) => {
options.MapHub<MyHub>("/hubs/myhub");
});
..
[Authorize]
public class MyHub : Hub
..
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5000",
"sslPort": 0
I start the debugger which brings the browser, register a user and log in, then go to http://localhost:5000/SignalRtest (my razor page that uses signalr.js) and verify the chat works fine.
I now try to create a .NET Core console app chat client:
class Program
{
public static async Task SetupSignalRHubAsync()
{
var hubConnection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/hubs/myhub")
.Build();
await hubConnection.StartAsync();
await hubConnection.SendAsync("Send", "consoleapp");
}
public static void Main(string[] args)
{
SetupSignalRHubAsync().Wait();
}
}
My issue is I don't know how to authenticate this client ?
EDIT:
(from https://github.com/aspnet/SignalR/issues/2455)
"The reason it works in the browser is because the browser has a
Cookie from when you logged in, so it sends it automatically when
SignalR makes it's requests. To do something similar in the .NET
client you'll have to call a REST API on your server that can set the
cookie, scoop the cookie up from HttpClient and then provide it in the
call to .WithUrl, like so:
var loginCookie = /* get the cookie */
var hubConnection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/hubs/myhub", options => {
options.Cookies.Add(loginCookie);
})
.Build();
I now put a bounty on this question, hoping to get a solution showing how to authenticate the .NET Core 2.1 SignalR console client with a .NET Core 2.1 web app SignalR server that uses "Identity as UI". I need to get the cookie from the server and then add it to SignalR HubConnectionBuilder (which now have a WithCookie method).
Note that I am not looking for a third-party solutions like IdentityServer
Thanks!
I did ended up getting this to work like this:
On the server I scaffolded Login and then in Login.cshtml.cs added
[AllowAnonymous]
[IgnoreAntiforgeryToken(Order = 1001)]
public class LoginModel : PageModel
That is I do not require the anti forgery token on login (which doesn't make sense anyway)
The client code is then like this:
HttpClientHandler handler = new HttpClientHandler();
CookieContainer cookies = new CookieContainer();
handler.CookieContainer = cookies;
HttpClient client = new HttpClient(handler);
var uri = new Uri("http://localhost:5000/Identity/Account/Login");
string jsonInString = "Input.Email=myemail&Input.Password=mypassword&Input.RememberMe=false";
HttpResponseMessage response = await client.PostAsync(uri, new StringContent(jsonInString, System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"));
var responseCookies = cookies.GetCookies(uri);
var authCookie = responseCookies[".AspNetCore.Identity.Application"];
var hubConnection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/hubs/myhub", options =>
{
options.Cookies.Add(authCookie);
})
.Build();
await hubConnection.StartAsync();
await hubConnection.SendAsync("Send", "hello!");
(of course password will be elsewhere in deployment)

how to upload images using web api 2 REST API

I am new to web api 2. I tried a lot but did not find proper working code for uploading images in web api 2.
I have a working code that is working in web api (old), but in web api 2, this particular value is null. HttpContext.Current
Can someone provide me working code?
Here is the controller in one of my Web API 2 projects:
namespace WebApi.Controllers
{
public class FilesController : ApiController
{
// POST api/<controller>
public async Task<HttpResponseMessage> Post()
{
// Check if the request contains multipart/form-data.
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
try
{
// Read the form data.
await Request.Content.ReadAsMultipartAsync(provider);
// This illustrates how to get the file names.
foreach (MultipartFileData file in provider.FileData)
{
Debug.Listeners[0].WriteLine(file.Headers.ContentDisposition.FileName);
Debug.Listeners[0].WriteLine("Server file path: " + file.LocalFileName);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
catch (System.Exception e)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
}
}
...
}
}
You can try by adding a Web API Controller class (v2.1).

404 trying to use web api endpoint

I'm trying to add a web api controller to my MVC project. It's an MVC 3 project that I've upgraded to MVC4. I'm trying to get the "test" simple api controller to work, and currently getting a 404. Here's what I've done:
I've added all the required packages.
I've added my webapi config to my Global Application_Start():
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
WebApiConfig.Register(GlobalConfiguration.Configuration); // Web API
This then calls my static Register method:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
I have a ApiController defined in my web app:
public class SitechangesController : ApiController
{
/// GET api/default1
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
And finally, when I build it all and browse to my site on localhost http://localhost/api/Sitechanges , I get a 404.
If I do a file/new project and create a web api project from scratch, I don't have these problems. Can anyone help?
Thanks
Matt
It seems adding the web api config before the "normal" routes fixes it!
WebApiConfig.Register(GlobalConfiguration.Configuration); // Moved to the top
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
Your controller must end in ...Controller.cs.
For example:
Test.cs and TestControllerV2.cs will return 404.
TestController.cs and TestV2Controller.cs will return 200.
I see yours does, but I came across your post when searching for why a 404 was returned.