Forward POST request from asp.net core controller to different URL - asp.net-core

I wanto to forward an incoming POST request to my asp.net core controller "as is" (including headers, body, from-data) to a different URL without using a middleware.
I found an example for doing that for asp.net: https://philsversion.com/2012/09/06/creating-a-proxy-with-apicontroller/
But this does not work for asp.net core, since the call to
return await http.SendAsync(this.Request);
in asp.net core accepts an HttpRequestMessage and the Request object is of type HttpRequest.
I also found some code, which creates a HttpRequestMessage from an HttpRequest, see: Convert Microsoft.AspNetCore.Http.HttpRequest to HttpRequestMessage
Using this code, the receiving endpoint (to which I forward to) gets the Body, but it does not get Form fields.
Checking the class HttpRequestMessage I saw that it does not contain a property for FormFields.
[Microsoft.AspNetCore.Mvc.HttpPost]
[NrgsRoute("api/redirect-v1/{key}")]
public async Task<HttpResponseMessage> Forward(
[FromUri] string key,
CancellationToken cancellationToken)
{
// the URL was shortened, we need to get the original URL to which we want to forward the POST request
var url = await _shortenUrlService.GetUrlFromToken(key, cancellationToken).ConfigureAwait(false);
using (var httpClient = new HttpClient())
{
var forwardUrl = new Uri(url);
Request.Path = new PathString(forwardUrl.PathAndQuery);
// see: https://stackoverflow.com/questions/45759417/convert-microsoft-aspnetcore-http-httprequest-to-httprequestmessage
var requestMessage = Request.ToHttpRequestMessage();
return await httpClient.SendAsync(requestMessage, cancellationToken);
// Problem: Forwards header and body but NOT form fields
}
}
Expected result would be that at my receiving endpoint I have the same
- headers
- body
- form fields
as in the original POST request.

I ended up doing the following:
[HttpPost]
[NrgsRoute("api/redirect-v1/{key}")]
public async Task<RedirectResult> Forward(string key, CancellationToken cancellationToken)
{
var url = await _shortenUrlService.GetUrlFromToken(key, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(url))
throw new BadRequestException($"Could not create forward URL from parameter {key}", "redirect-error");
using (var httpClient = new HttpClient())
{
var forwardUrl = new Uri(url);
Request.Path = new PathString(forwardUrl.PathAndQuery);
HttpResponseMessage responseMessage;
if (Request.HasFormContentType)
responseMessage = await ForwardFormData(key, httpClient, forwardUrl, cancellationToken);
else
responseMessage = await ForwardBody(key, httpClient, cancellationToken);
var queryParams = forwardUrl.GetQueryStringParams();
var lUlr = queryParams["lurl"];
return new RedirectResult(lUlr);
}
}
private async Task<HttpResponseMessage> ForwardFormData(string key, HttpClient httpClient, Uri forwardUrl, CancellationToken cancellationToken)
{
var formContent = new List<KeyValuePair<string, string>>();
HttpResponseMessage result;
if (Request.ContentType == "application/x-www-form-urlencoded")
{
foreach (var formKey in Request.Form.Keys)
{
var content = Request.Form[formKey].FirstOrDefault();
if (content != null)
formContent.Add(new KeyValuePair<string, string>(formKey, content));
}
var formUrlEncodedContent = new FormUrlEncodedContent(formContent);
result = await httpClient.PostAsync(forwardUrl, formUrlEncodedContent, cancellationToken);
}
else
{
var multipartFormDataContent = new MultipartFormDataContent();
foreach (var formKey in Request.Form.Keys)
{
var content = Request.Form[formKey].FirstOrDefault();
if (content != null)
multipartFormDataContent.Add(new StringContent(content), formKey);
}
result = await httpClient.PostAsync(forwardUrl, multipartFormDataContent, cancellationToken);
}
return result;
}
private async Task<HttpResponseMessage> ForwardBody(string key, HttpClient httpClient, CancellationToken cancellationToken)
{
// we do not have direct access to Content, see: https://stackoverflow.com/questions/41508664/net-core-forward-a-local-api-form-data-post-request-to-remote-api
var requestMessage = Request.ToHttpRequestMessage();
return await httpClient.SendAsync(requestMessage, cancellationToken);
}

Related

Content Type Header in Azure Function

How can I return HTTP Content-Type "application/json" header? Can not find a sample in net...
[FunctionName("Function1")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
Dictionary<string, string> dd = Parser(requestBody);
string json = JsonConvert.SerializeObject(dd);
if (json == null)
{
return new BadRequestObjectResult("Please pass request body");
}
return (ActionResult)new OkObjectResult(json);
}
1 - By specifying it in your request "Accept : application/json". Azure functions will natively return the type requested in the Accept header. Your code should already be correctly honouring that request.
2 - By returning a JsonResult
The following code will ignore the Accept header and return "application/json" in every case - your serialization is redundant.
[FunctionName("Function1")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
Dictionary<string, string> dd = Parser(requestBody);
if (dd == null) return new BadRequestObjectResult("Please pass request body");
return (ActionResult) new JsonResult(dd);
//return (ActionResult) new OkObjectResult(dd);
}
You could do this by accessing the Response object via the request's HttpContext:
[FunctionName("Function1")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
Dictionary<string, string> dd = Parser(requestBody);
string json = JsonConvert.SerializeObject(dd);
if (json == null)
{
return new BadRequestObjectResult("Please pass request body");
}
//add this line...
req.HttpContext.Response.Headers.Add("Content-Type", "application/json");
return (ActionResult)new OkObjectResult(json);
}
I believe you are looking for the CreateResponse class:
var jObject = JObject.Parse(resp);
var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(jObject.ToString(), Encoding.UTF8, "application/json");
return response;

Asp.Net Core - Making API calls from backend

I have an application which is calling API's from a backend cs class, using IHostedService. With basic API calls ("http://httpbin.org/ip") it is working fine and returning the correct value, however I now need to call a Siemens API which requires me to set an Authorization header, and place "grant_type=client_credentials" in the body.
public async Task<string> GetResult()
{
string data = "";
string baseUrl = "https://<space-name>.mindsphere.io/oauth/token";
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", {ServiceCredentialID: ServiceCredentialSecret});
using (HttpResponseMessage res = await client.GetAsync(baseUrl))
{
using (HttpContent content = res.Content)
{
data = await content.ReadAsStringAsync();
}
}
}
I think I have the header set up correctly but I won't know for sure until the full request gets formatted. Is it even possible to set the the body of the request to "grant_type=client_credentials"?
As far as I can see from Siemens API documentation they expect Form data, so it should be like:
public async Task<string> GetResult()
{
string data = "";
string baseUrl = "https://<space-name>.mindsphere.io/oauth/token";
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", {ServiceCredentialID: ServiceCredentialSecret});
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials")
});
using (HttpResponseMessage res = await client.PostAsync(baseUrl, formContent))
{
using (HttpContent content = res.Content)
{
data = await content.ReadAsStringAsync();
}
}
}
}

HttpClient with multiple proxies

How can one use HttpClient with a pipeline of multiple proxies?
A single proxy can be handled via HttpClientHandler:
HttpClient client1 = new HttpClient(new HttpClientHandler()
{
Proxy = new WebProxy()
{
Address = new Uri($"http://{proxyIp}:{proxyPort}"),
BypassProxyOnLocal = false,
UseDefaultCredentials = false
}
});
I want the requests to pass through multiple proxies.
I already tried subclassing DelegatingHandler like this:
public class ProxyDelegatingHandler : DelegatingHandler
{
public ProxyDelegatingHandler(string proxyIp, int proxyPort):
base(new HttpClientHandler()
{
Proxy = new WebProxy()
{
Address = new Uri($"http://{proxyIp}:{proxyPort}"),
BypassProxyOnLocal = false,
UseDefaultCredentials = false
}
})
{
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken);
}
}
And passing the list to factory, but it throws an exception which is probably caused by incorrect implementation of ProxyDelegatingHandler:
var handlers = new List<DelegatingHandler>();
handlers.Add(new ProxyDelegatingHandler(ip1, port2));
handlers.Add(new ProxyDelegatingHandler(ip2, port2));
HttpClient client = HttpClientFactory.Create(handlers.ToArray())
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
var res = await client.SendAsync(requestMessage);
Exception:
The 'DelegatingHandler' list is invalid because the property 'InnerHandler' of 'CustomHandler' is not null. Parametername: handlers
Related Post: link

Azure web api Unauthorized 401

I have some code that used to call Azure Scheduler to get a token, then using that token, make restful calls. Works a treat.
So i decided to adopt the code into a new app but this time call my own web api hosted on azure. The API is registered in Active directory I have created a secret key etc. When i initiliaze my static httpclient it fetches a token succesfully.
But when i make a call to the API using the token for auth, the response is a 401 "unauthorized", below is the code.
public static class SchedulerHttpClient
{
const string SPNPayload = "resource={0}&client_id={1}&grant_type=client_credentials&client_secret={2}";
private static Lazy<Task<HttpClient>> _Client = new Lazy<Task<HttpClient>>(async () =>
{
string baseAddress = ConfigurationManager.AppSettings["BaseAddress"];
var client = new HttpClient();
client.BaseAddress = new Uri(baseAddress);
await MainAsync(client).ConfigureAwait(false);
return client;
});
public static Task<HttpClient> ClientTask => _Client.Value;
private static async Task MainAsync(HttpClient client)
{
string tenantId = ConfigurationManager.AppSettings["AzureTenantId"];
string clientId = ConfigurationManager.AppSettings["AzureClientId"];
string clientSecret = ConfigurationManager.AppSettings["AzureClientSecret"];
string token = await AcquireTokenBySPN(client, tenantId, clientId, clientSecret).ConfigureAwait(false);
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); //TODO ssmith: const or localization
}
private static async Task<string> AcquireTokenBySPN(HttpClient client, string tenantId, string clientId, string clientSecret)
{
var payload = String.Format(SPNPayload,
WebUtility.UrlEncode(ConfigurationManager.AppSettings["ARMResource"]),
WebUtility.UrlEncode(clientId),
WebUtility.UrlEncode(clientSecret));
var body = await HttpPost(client, tenantId, payload).ConfigureAwait(false);
return body.access_token;
}
private static async Task<dynamic> HttpPost(HttpClient client, string tenantId, string payload)
{
var address = String.Format(ConfigurationManager.AppSettings["TokenEndpoint"], tenantId);
var content = new StringContent(payload, Encoding.UTF8, "application/x-www-form-urlencoded");
using (var response = await client.PostAsync(address, content).ConfigureAwait(false))
{
if (!response.IsSuccessStatusCode)
{
Console.WriteLine("Status: {0}", response.StatusCode);
Console.WriteLine("Content: {0}", await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}//TODO: start removing tests
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<dynamic>().ConfigureAwait(false);
}
}
}
The above code is the class that creates a httpclient and gets its authorization.
public virtual async Task<T> GetAsync(string apiURL)
{
try
{
_client = await SchedulerHttpClient.ClientTask;
var response = await _client.GetAsync(apiURL);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsAsync<T>().ConfigureAwait(false);
return responseContent;
}
catch (Exception e)
{
return default(T);
}
}
The above code is a quick lift of my old code simply to test if i can get any results. but as stated it returns a 401.
My question is, is my old code to get authorization incorrect?
<add key="ARMResource" value="https://management.core.windows.net/" />
<add key="TokenEndpoint" value="https://login.windows.net/{0}/oauth2/token" />
<add key="BaseAddress" value="https://mysite.azurewebsites.net" />
As suspected, This particular issue was cause by the incorrect "ARMresource" in the case of a web api it required me to change it to the client id.
Source of answer
Seems my issue was the same, however i suspect i may be able to omit the resource entirely from my SPNPayload string.

How to receive a response package from GET request for OneNote API

I'm getting a acknowledgement but no response message (details) i.e. list of notebooks from the OneNote API. Below is my code. I am able to receive the header and JSON details from a POST message but not the GET. I have tried to convert the POST code in order to submit a GET request.
private async void getRequestClick(object sender, RoutedEventArgs e)
{
await GetRequests(true, "test");
}
async public Task<StandardResponse> GetRequests(bool debug, string sectionName)
{
Uri PagesEndPoint1 = new Uri("https://www.onenote.com/api/v1.0/notebooks");
var client = new HttpClient();
//// Note: API only supports JSON return type.
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//// This allows you to see what happens when an unauthenticated call is made.
if (IsAuthenticated)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authClient.Session.AccessToken);
}
HttpResponseMessage response;
HttpRequestMessage createMessage = new HttpRequestMessage(HttpMethod.Get, PagesEndPoint1);
response = await client.SendAsync(createMessage);
tbResponse.Text = response.ToString();
return await TranslateResponse(response);
}
private async static Task<StandardResponse> TranslateResponse(HttpResponseMessage response)
{
StandardResponse standardResponse;
if (response.StatusCode == HttpStatusCode.Created)
{
dynamic responseObject = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
standardResponse = new CreateSuccessResponse
{
StatusCode = response.StatusCode,
OneNoteClientUrl = responseObject.links.oneNoteClientUrl.href,
OneNoteWebUrl = responseObject.links.oneNoteWebUrl.href
};
}
else
{
standardResponse = new StandardErrorResponse
{
StatusCode = response.StatusCode,
Message = await response.Content.ReadAsStringAsync()
};
}
// Extract the correlation id. Apps should log this if they want to collcet the data to diagnose failures with Microsoft support
IEnumerable<string> correlationValues;
if (response.Headers.TryGetValues("X-CorrelationId", out correlationValues))
{
standardResponse.CorrelationId = correlationValues.FirstOrDefault();
}
return standardResponse;
}
My POST messages are working OK. I can create a new page etc.
I think you need to change the expected status code from HttpStatusCode.Created to HttpStatusCode.OK for Get requests, since they return a 200 and not a 201. Try doing that in your TranslateResponse method.