Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.NET https requests with different security protocols across threads

I maintain a quite complex ASP.NET application (a customized NopCommerce 3.10). It needs to connect to third-party servers through HTTPS on different scenarios. I am doing this via the HttpWebRequest class.

Some of these servers are poorly configured:

One of the third-party servers (say Server A) requires SSL3 protocol type and simply fails the connection if another protocol type is set. Another server (say Server B) provides an incorrect certificate if connection is performed with SSL3. More exactly, it provides a certificate with a wrong CN (common name). However, if I use TLS from the start, the certificate is OK.

I determined the issue above using the ServicePointManager.ServerCertificateValidationCallback callback to check the SSL policy error.

Changing the security protocol is done via ServicePointManager.SecurityProtocol which is a static property. However, the requests performed by clients to my application which trigger the HTTPS connections described above can happen to be run in parallel in different threads.

If I, for example: set the security protocol to the desired type, perform the HTTPS request, then set it back for Server A, I have no guarantee that if a request in the meanwhile needs to connect to Server B does not change the ServicePointManager.SecurityProtocol to a value other than the one needed by Server A. I believe this is a typical multi-threading issue with static variables.

From my research, I determined that .NET does not provide a mean to use a specific SSL protocol for each WebRequest instance.

I'm thinking about solutions such as:

  • queuing all outgoing HTTPS connections inside my application to ensure proper SSL protocol for each one
  • constructing a separate app-domain for each HTTPS request (suggested by https://stackoverflow.com/a/3107692/1288522)
  • change HTTPS request to low-level TCP connections and enforce different SSL protocols for each of them
  • making a proxy asp.net application which will queue the outgoing requests

Note: Queuing would not be a huge performance hit because a small percentage of all client requests actually reach the code in question.

The solutions above, however, require difficult refactoring considering the app architecture or rough workarounds (the third solution)

My question is very similar to this one on msdn, however that one did not get satisfactory answers.

Is there a more straight-forward or efficient way of ensuring that each https request uses a specific SSL protocol?

like image 633
Dan Mirescu Avatar asked Oct 02 '14 11:10

Dan Mirescu


1 Answers

We faced this same issue, and went for the app-domain approach you mentioned, implementing a solution based on what is proposed here, which is a really nice write up of how to manage the execution of code in a separate app domain:

http://www.superstarcoders.com/blogs/posts/executing-code-in-a-separate-application-domain-using-c-sharp.aspx

We are using his Isolated class pretty much as is:

  public sealed class Isolated<T> : IDisposable where T : MarshalByRefObject
  {
    private AppDomain _domain;
    private readonly T _value;

    public Isolated()
    {
        _domain = AppDomain.CreateDomain("Isolated:" + Guid.NewGuid(), null, AppDomain.CurrentDomain.SetupInformation);

        var type = typeof(T);

        _value = (T)_domain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName);
    }

    public T Value
    {
        get
        {
            return _value;
        }
    }

    public void Dispose()
    {
        if (_domain == null) return;

        AppDomain.Unload(_domain);

        _domain = null;
    }
}

And then we have a wrapper around the standard WebClient, that allows for the setting of the protocol:

public class WebClient : MarshalByRefObject, IWebClient
{
    public WebClientResponse GetResponse(string address)
    {
        return GetResponse(address, null);
    }

    public WebClientResponse GetResponse(string address, string securityProtocol)
    {
        if (!string.IsNullOrWhiteSpace(securityProtocol))
            ServicePointManager.SecurityProtocol = (SecurityProtocolType)Enum.Parse(typeof(SecurityProtocolType), securityProtocol);

        var response = new WebClientResponse();

        try
        {
            using (var wc = new System.Net.WebClient())
            {
              // <do stuff>
            }
        }
        catch (Exception ex)
        {
            response.Exception = new GetResponseException(string.Format("Unable to get response from {0}", address), ex);
        }

        return response;
    }
}

[Serializable]
public class WebClientResponse
{
    public Exception Exception { get; set; }

    public string Response { get; set; }
}

 [Serializable]
public class GetResponseException : Exception
{
    public GetResponseException(string message, Exception innerException)
        : base(message, innerException)
    {
    }

    public GetResponseException(SerializationInfo info, StreamingContext context)  : base(info, context)
    {
    }
}

Tying them together, we have code that determines if it needs to override the protocol that's currently set. If so, it spins up the isolated app domain, if not it uses an existing WebClient:

...
        WebClientResponse webClientResponse;

        if (!string.IsNullOrWhiteSpace(ForceSecurityProtocol))
        {
            using (var isolated = new Isolated<WebClient>())
            {
                webClientResponse = isolated.Value.GetResponse(url, ForceSecurityProtocol);
            }
        }
        else
        {
            webClientResponse = _webClient.GetResponse(url);
        }
...

Note, our use isn't in a tremendously high throughput area of our application, so whatever performance price we're paying using this approach is really non-impactful. If we were going to put something like this in a place where it been in the way of a significant amount of traffic through our web app, we would have done some testing.

like image 127
Veatch Avatar answered Nov 19 '22 20:11

Veatch