I have a strange error, that I can't reproduce on my own computer.
Here is the complete code I'm using:
public async Task LoadHeaderAndFooter()
{
//ignore any SSL errors
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
var baseAddress = new Uri(Request.Url.GetComponents(UriComponents.Scheme | UriComponents.Host, UriFormat.Unescaped));
var cookieContainer = new CookieContainer();
using (var handler = new HttpClientHandler { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
{
var oCookies = Request.Cookies;
for (var j = 0; j < oCookies.Count; j++)
{
var oCookie = oCookies.Get(j);
if (oCookie == null) continue;
var oC = new Cookie
{
Domain = baseAddress.Host,
Name = oCookie.Name,
Path = oCookie.Path,
Secure = oCookie.Secure,
Value = oCookie.Value
};
cookieContainer.Add(oC);
}
Header.Text = await client.GetStringAsync("/minside/loyalityadmin/header");
Footer.Text = await client.GetStringAsync("/minside/loyalityadmin/footer");
}
}
What happens is that the request starts, then waits for the timeout (30 sek default), the httpclient throw's a "task canceled" exeption. THEN the actuall request fires on the server.
Now this code is run in an .ascx.cs file. While the /header is a MVC controll with a .cshtml view. Both run on the same server.
How can I get this to work?
After even more testing it seems like restsharp.org also have the same issues on some plattforms (everywhere but my development plattform).
Turns out the problem is the session object. On IIS it is not possible to read the session state in parallell:
Access to ASP.NET session state is exclusive per session, which means
that if two different users make concurrent requests, access to each
separate session is granted concurrently. However, if two concurrent
requests are made for the same session (by using the same SessionID
value), the first request gets exclusive access to the session
information. The second request executes only after the first request
is finished. (The second session can also get access if the exclusive
lock on the information is freed because the first request exceeds the
lock time-out.) If the EnableSessionState value in the # Page
directive is set to ReadOnly, a request for the read-only session
information does not result in an exclusive lock on the session data.
However, read-only requests for session data might still have to wait
for a lock set by a read-write request for session data to clear.
from https://msdn.microsoft.com/en-us/library/ms178581.aspx
So the solution was to simply not pass the session id:
if (oCookie == null || oCookie.Name == "ASP.NET_SessionId") continue;
Witch works fine, since the secondary request did not need access to the session. Any solution that makes it possible to use the session in both request would be recieved with thanks.
Related
I use a C# HttpClient to access an REST-API-endpoint with a PKI certificate authentication/authorization.
The connection works for a certain number of queries without problems. According to the endpoint, there are some limits, which I list here for completeness:
Number of requests for an already established TLS connection = 200
Timeout how long to wait to be able to send another request over the same connection = 5 seconds
The HttpClient will be instanced once at starting the application.
private HttpClientHandler _httpClientHandler;
private static HttpClient _httpClient;
public MyTestClass()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
_httpClientHandler = new HttpClientHandler();
_httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
_httpClient = new HttpClient(_httpClientHandler);
}
And the request method looks like this:
private HttpResponseMessage PerformRequest(string apiCommand, Entity.EntityTypes entityType, List<KeyValuePair<Entity.Attributes, string>> attributes)
{
try
{
string relativeUri = string.Format("{0}/{1}?{2}&{3}",
apiCommand,
(apiCommand == Entity.API_SELECT) ? Entity.GetEntityCommand(entityType) : string.Empty,
SYSTEM_AUTHENTICATION_PRODUCTION,
PKI_USER_AUTHENTICATION);
foreach (KeyValuePair<Entity.Attributes, string> attribute in attributes)
relativeUri += "&" + Entity.GetAttributeCommand(entityType, attribute.Key) + attribute.Value;
return _httpClient.GetAsync(relativeUri).Result;
}
catch (Exception ex)
{
throw new ClientException(ex.Message, ex.InnerException);
}
finally
{
LogReturn(CALL2);
}
}
At the end of the day, the user is able to select a bunch of records from a grid and uses a method which is calling the PerformRequest method at the end. The PerformRequest will be called in a loop and the response will be analyzed every run.
The call looks like this:
public string GetInfos(string partNo)
{
HttpResponseMessage response = new HttpResponseMessage();
string result = null;
try
{
List<KeyValuePair<Entity.Attributes, string>> attributes = new List<KeyValuePair<Entity.Attributes, string>>();
attributes.Add(new KeyValuePair<Entity.Attributes, string>(Entity.Attributes.PartKey, PartNo));
response = PerformRequest(Entity.API_SELECT, Entity.EntityTypes.Part, attributes);
if (response.Content.Headers.ContentType.MediaType == MIME_TYPE_TEXT)
{
result = (response.Content.ReadAsStringAsync()).Result;
}
}
finally
{
response.Dispose();
}
return result;
}
The problem occurs after > 200 runs of the PerformRequest method (some times 218, another time 220, ...). There will be thrown a WebException: "The request was aborted: Could not create SSL/TLS secure channel".
The surprise is, that no further call of the PerformRequest is working from now. All new calls ends in the same WebException. If I close the application und open it again, all is good and I can call the loop again.
The bigger surprise is, that if I copy the same REST-API-url which will be used by the application and paste it to a browser and excecute it, then it does take some time (I think the browser has to load the pki certificate from certificate store), then it loads the data correctly and afterwards my application also can do the loop one more again.
I did try everything I read from "using blocks" to non "using blocks" of the HttpClient/HttpClientHandler and ResponseMessages. I tried to pick the certificate manually by changing from HttpClientHandler to WebRequestHandler. Using the ServicePoint.ConnectionLeaseTimeout property, also the HttpClientHandler.PreAuthenticate and HttpClient.DefaultRequestHeaders.ConnectionClose, ServicePointManager.Expect100Continue properties etc. I did this the last two days and I don't know what I can do further more.
Does anyone happen to have an idea what else it can be or why this does not work?
And why does it work again if the call took place in the browser?
Thank you very much for your support!
I'm looking for a way to use an existing session ID with the ServiceStack ServerEventsClient. I'd like to use server events, but access will need to be limited to authenticated users.
For JsonServiceClient, I have a method in a library referenced by all of our projects which returns an authenticated JsonServiceClient ready to be used. The purpose is to make development a bit faster by keeping all auth info in one place:
public static JsonServiceClient GetAuthenticatedServiceClient()
{
var ServiceClient = new JsonServiceClient(globals.ApiUrl)
{
RequestFilter = request => request.UserAgent = globals.ClientSoftware.ToString()
};
var CookieQuery = (from c in globals.AuthCookieContainer
where c.Name == "ss-id"
where c.Expires > DateTime.Now
select c);
if (CookieQuery.Count() > 0)
{
ServiceClient.CookieContainer.Add(CookieQuery.FirstOrDefault());
}
else
{
throw new Exceptions.ApiNotAuthenticatedException();
}
return ServiceClient;
}
My question is: Is there a way to implement something similar to the above method for ServerEventsClient? I'm trying to avoid sending an Authenticate request, since I've already got an easy way to get to session information on the client.
The ServerEventsClient.ServiceClient used to maintain the CookieContainer for each ServerEvents request is just a JsonServiceClient so you can access its concrete type with just:
var client = (JsonServiceClient)serverEventsClient.ServiceClient;
So you can take the same approach of transferring cookies between any service client.
The ServerEventsClient.Authenticate literally just makes an Authenticate request on the same ServerEventsClient.ServiceClient instance so it gets populated with the returned cookies from a successful response.
If you start a new Web Project, and create a new MVC4 application (with sub-kind as "WebApi", you can paste the below code in (overwriting HomeController.cs) to get the code to work.
I have a MVC4 application (with WebApi).
I am trying to set a custom-header in a MVC controller method and then do a RedirectToAction. The custom-header is not seen in the second mvc-controller-method.
I am able to set a cookie in the first mvc-controller-method and see it in the second mvc-controller-method (after a RedirectToAction).
Is there a way to see the custom-header I set in the second mvc-controller-method after a RedirectToAction ?
Thanks.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
namespace MyMvc4WebApiProjectNamespace.Controllers
{
public class HomeController : Controller
{
private const string CustomCookieName = "CustomCookieName";
private const string CustomHeaderName = "X-CustomHeaderName";
private const string IISExpressRootUrl = "http://localhost:55937/"; /* open up the project properties and go to the web tab and find the iis-express area to get the correct value for your environment */
public ActionResult Index()
{
IEnumerable<string> webApiValues = null;
string value1 = null;
string value2 = null;
HttpClientHandler handler = new HttpClientHandler
{
UseDefaultCredentials = true,
PreAuthenticate = true
};
using (var client = new HttpClient(handler))
{
string valuesUri = IISExpressRootUrl + "api/Values";
webApiValues = client
.GetAsync(valuesUri)
.Result
.Content.ReadAsAsync<IEnumerable<string>>().Result;
if (null != webApiValues)
{
value1 = webApiValues.ElementAt(0);
value2 = webApiValues.ElementAt(1);
}
else
{
throw new ArgumentOutOfRangeException("WebApi call failed");
}
}
HttpCookie customCookie = new HttpCookie(CustomCookieName, "CustomCookieValue_ThisShowsUpIn_MyHomeControllerAlternateActionResult_Method");
Response.Cookies.Add(customCookie);
HttpContext.Response.AppendHeader(CustomHeaderName, "CustomHeaderValue_This_Does_Not_Show_Up_In_MyHomeControllerAlternateActionResult_Method");
//Response.AppendHeader(CustomHeaderName, value2);
return RedirectToAction("MyHomeControllerAlternateActionResult");
}
public ActionResult MyHomeControllerAlternateActionResult()
{
IEnumerable<string> webApiReturnValues = null;
CookieContainer cookieContainer = new CookieContainer();
foreach (string cookiename in Request.Cookies)
{
if (cookiename.Equals(CustomCookieName, StringComparison.OrdinalIgnoreCase))
{
var cookie = Request.Cookies[cookiename];
cookieContainer.Add(new Cookie(cookie.Name, cookie.Value, cookie.Path, "localhost"));
}
}
if (cookieContainer.Count < 1)
{
throw new ArgumentOutOfRangeException("CookieContainer did not find the cookie I was looking for");
}
else
{
Console.WriteLine("This is what actually happens. It finds the cookie.");
}
HttpClientHandler handler = new HttpClientHandler
{
UseCookies = true,
UseDefaultCredentials = true,
PreAuthenticate = true,
CookieContainer = cookieContainer
};
using (var client = new HttpClient(handler))
{
bool customHeaderWasFound = false;
if (null != this.Request.Headers)
{
if (null != this.Request.Headers[CustomHeaderName])
{
IEnumerable<string> headerValues = this.Request.Headers.GetValues(CustomHeaderName);
client.DefaultRequestHeaders.Add(CustomHeaderName, headerValues);
customHeaderWasFound = true;
}
}
/*I wouldn't expect it to be in the below, but I looked for it just in case */
if (null != this.Response.Headers)//
{
if (null != this.Response.Headers[CustomHeaderName])
{
IEnumerable<string> headerValues = this.Response.Headers.GetValues(CustomHeaderName);
client.DefaultRequestHeaders.Add(CustomHeaderName, headerValues);
customHeaderWasFound = true;
}
}
if (!customHeaderWasFound)
{
Console.WriteLine("This is what actually happens. No custom-header found. :( ");
}
string valuesUri = IISExpressRootUrl + "api/Values";
webApiReturnValues = client
.GetAsync(valuesUri)
.Result
.Content.ReadAsAsync<IEnumerable<string>>().Result;
if (null == webApiReturnValues)
{
throw new ArgumentOutOfRangeException("WebApi call failed");
}
}
return View(); /* this will throw a "The view 'MyHomeControllerAlternateActionResult' or its master was not found or no view engine supports the searched locations" error, but that's not the point of this demo. */
}
}
}
Response headers are never copied automatically to requests - so setting any custom headers on response will not impact next request issued to handle 302 redirect.
Note that it is the case even with cookies: response comes with "set this cookie" header, and all subsequent request will get "current cookies" header.
If you have your own client you may be able to handle 302 manually (not possible if you are using browser as client).
As another answer stated, response headers are about this response, not the next one. Redirecting is not a server-side action. A redirect instructs the client to perform a completely new request, and of course in a new request, the response headers for the old request are not present. So return RedirectToAction("MyHomeControllerAlternateActionResult"); is guaranteed to not have this response's headers when the browser initiates the new request.
In trying to solve this problem, one might think of trying to persist the data to the next request server-side, such as through a cookie or in an explicit session variable, or implicitly via use of ViewBag/ViewData/TempData. However, I don't recommend this as using session state heavily has performance implications in large/high-usage web sites, plus there are other negative and subtle side-effects that you may run into down the road. For example, if a person has two browser windows open to the same web site, they can't be doing different actions reliably, as the session data for one window can end up being served to the other one. Avoid session usage as much as possible in your web site design—I promise this will benefit you down the road.
A slightly better way, though still with its problems, is to redirect to a URL with querystring parameters containing a payload. And, instead of the whole set of data, you can provide a key that can be pulled from the session (as long as it's also bound to their IP address and is large like a GUID or two together). However, relying on session state is still not ideal as stated before.
Instead, consider using server-side redirection such as child actions. If you find that hard because what you want to call is a main controller you have a few options:
If you're using dependency injection, add a parameter to the current controller (saving it from the constructor and using it in the request method) that is the desired controller you want to "redirect" to. You can then call that controller directly. This may not be ideal (as all calls to this controller also have to new up a copy of that one), but it does work. Trying to new up the other controller manually can also work, but for reasons I don't fully remember, I think this can give some additional problems. In any case, this method can give issues accessing the HttpRequest context and other context objects correctly, though this can be worked around.
Rearchitect your application so that controllers are not the place where full pages are rendered. Instead, use them as "smart routers" that call child actions to perform the real work. Then, you can call the same child actions from any controller. But this still has problems.
Perhaps the best way is to add custom routing logic through action filters or other means (search the web!) so that the correct controller is hit in the first place! This may not always be possible, but sometimes the need to redirect to another controller mid-procedure actually points to a larger design problem. Focusing on how to cause the knowledge of which controller to hit to be available earlier in the pipeline (such as during routing) can reveal architecture problems, and can reveal likely solutions to them.
There may be other options that I haven't thought of, but at least you have a few alternatives to the simple "no way to do that."
I was able to do something similar like what the user is requesting in the following (rudimentary) way:
In the redirect, add a custom query string parameter
Create a custom Module that checks for that parameter and appends the custom header (read http://dotnetlionet.blogspot.com/2015/06/how-to-add-httpmodule-in-mvc5.html on how to do your own module)
In this way I was able to get my custom headers to be picked up
I'm wondering if anyone has solved managing the expiration of Google OAuth2 tokens?
The example below is a REST call to get a list of calendars with a valid token. If the token is expired, I will get a 401 response and need to refresh the token using the 'refresh token' stored in my database. I'm wondering if anyone has a strategy around this for their web application?
var httpContent = new HttpRequestMessage(HttpMethod.Get, "https://www.googleapis.com/calendar/v3/users/me/calendarList");
httpContent.Headers.Add("Authorization", "OAuth " + token);
responseBody = client.SendAsync(httpContent).Result.EnsureSuccessStatusCode().Content.ReadAsStringAsync().Result;
I've thought about refreshing automatically upon user logging in and saving the refresh token encrypted in session, but not sure if there are any better strategies.
//Notes
Strategy Options:
1 - Upon Logging a user in, refresh all OAuth tokens for user. This works assuming that a session will never last longer than a token timeout period. (may not be reliable for all OAuth servers).
2 - When refreshing, use token expiration to record expiration date/time in database. Before calling any API, check if token needs to be refreshed. (still need to account for edge case where token expires unexpectedly outside of normal expiration schedule)
3 - Catch the response status of the call and check for 401s. If receive a 401, refresh the token and try again. This could be a fail-over for both Options 1/2. Code for this example would be here:
var restClient = new RestClient();
var request = new RestRequest("https://www.googleapis.com/calendar/v3/users/me/calendarList", Method.GET);
request.AddHeader("Authorization", "OAuth " + token);
// execute the request
var response = restClient.Execute(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var newToken = RefreshGoogleToken(token);
request = new RestRequest("https://www.googleapis.com/calendar/v3/users/me/calendarList", Method.GET);
request.AddHeader("Authorization", "OAuth " + newToken);
// execute the request
response = restClient.Execute(request);
}
var content = response.Content; // raw content as string
dynamic responseJson = JsonValue.Parse(content);
var calendarList = new List<GoogleCalendar>();
foreach (var item in responseJson.items)
{
var calendar = new GoogleCalendar { Kind = item.kind, Etag = item.etag, Id = item.id, Title = item.summary, Description = item.description, Location = item.location, Timezone = item.timeZone, SummaryOverride = item.summaryOverride, ColorId = item.colorId, AccessRole = item.accessRole };
if (item.defaultReminders != null)
{
calendar.DefaultReminders = new List<GoogleCalendarReminder>();
foreach (var reminder in item.defaultReminders)
{
var rem = new GoogleCalendarReminder { Method = reminder.method, Minutes = reminder.minutes };
calendar.DefaultReminders.Add(rem);
}
}
calendarList.Add(calendar);
}
return calendarList;
}
If the expiration time is known you can keep track of your token's expiration time and anticipate that it has already expired, then just do the refresh at that time.
I would imagine you could also simply respond to the 401 errors with a refresh as well, and even as a fail safe if the expiration tracking happens to fail (just be sure to add code to avoid getting stuck in a loop).
Here is how my system is set up:
A web service using ASP.Net web service
The web service has a web method with EnableSession=true
A client which refers to the web service using "Service References" (note: not "Web References")
The app.config of the client has allowCookies=true
On the client side, I have the following code to upload a file to the service
bool res = service.InitiateUpload();
if (res) {
do {
read = stream.Read(buffer, 0, BLOCK_SIZE);
if (read == BLOCK_SIZE)
res = res && service.AppendUpload(buffer);
else if (read > 0)
// other call to AppendUpload, after buffer manipulation
On the server side, I have code that checks if the session id from the call to InitiateUpload is the same as AppendUpload.
[WebMethod(EnableSession=true)]
public bool InitiateUpload() {
lock (theLock) {
if (IsImportGoingOn)
return false;
theImportDataState = new ImportDataState(Session.SessionID);
}
return true;
}
[WebMethod(EnableSession=true)]
public bool AppendUpload(byte[] data) {
lock (theLock) {
if (!IsImportGoingOn)
return false;
if (theImportDataState.session != Session.SessionID)
return false;
theImportDataState.buffer.AddRange(data);
return true;
}
}
The call to AppendUpload returns false, because of the mismatching session ids. Why is that?
As far as I can see, I have the right attributes for the web method, the client has the correct config, and the same instance of the proxy is used. Am I missing something?
The trick is you need to make the service client aware it should care about cookies -- it doesn't by default.
Perhaps a better method would be for the service to pass back a transaction ID the client could use to reference the upload in the future. It would be cleaner in the long run and probably be less work than making the client work with cookies and therefore sessions.
You have to use CookieContainer to retain the session.
private System.Net.CookieContainer CK = new System.Net.CookieContainer();
Then just use this as your service object's CookieContainer.