using System;
using System.Configuration;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Xml;
namespace Neo.LegitimateLicences.Logic.eNatis
{
///
/// Centralised, process-wide eNatis session lifecycle. Guarantees AT MOST ONE
/// concurrent eNatis session across the whole app instance by serialising all
/// calls through a semaphore and caching the JSESSIONID cookie in a static field.
///
/// Eliminates the previous pattern where every HTTP request to our app caused
/// a fresh eNatis OPEN/CLOSE cycle, leading to MAX_SESSIONS_IN_USE under load.
///
/// Use for every eNatis HTTP interaction. The caller
/// supplies the actual query work; this class supplies a logged-in cookie and
/// ensures the session is reused / refreshed / retried correctly.
///
public static class ENatisSession
{
private static readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
private static string _cookie = string.Empty;
private static DateTime _lastUsedUtc = DateTime.MinValue;
private static readonly TimeSpan _idleTimeout = TimeSpan.FromMinutes(5);
private static readonly int[] _retryDelaysMs = { 2000, 4000, 8000 };
private static readonly string _loginURL = ConfigurationManager.AppSettings["eNatisLoginURL"];
/// The current JSESSIONID cookie string, or empty if no session is active.
public static string Cookie { get { return _cookie; } }
///
/// Runs under the eNatis session. Acquires the gate,
/// ensures a fresh cookie, hands it to , and releases.
/// Retries up to 3 times with exponential backoff if eNatis reports
/// MAX_SESSIONS_IN_USE during login.
///
/// The eNatis HTTP work to perform, given the active cookie.
/// Result of .
public static T WithSession(Func work)
{
if (work == null) throw new ArgumentNullException("work");
for (int attempt = 0; ; attempt++)
{
_gate.Wait();
bool succeeded = false;
try
{
EnsureFreshSessionLocked();
var result = work(_cookie);
_lastUsedUtc = DateTime.UtcNow;
succeeded = true;
return result;
}
catch (MaxSessionsInUseException)
{
// Drop any cookie we might have so the next attempt logs in fresh.
ForceLogoutLocked();
if (attempt >= _retryDelaysMs.Length) throw;
}
finally
{
_gate.Release();
}
if (!succeeded) Thread.Sleep(_retryDelaysMs[attempt]);
}
}
///
/// Ensures an active eNatis session exists (logs in if cookie is empty or
/// stale). Throws if eNatis returns
/// MAX_SESSIONS_IN_USE for all retry attempts.
///
public static void EnsureSession()
{
WithSession(_ => 0);
}
///
/// Eagerly logs the current eNatis session out (best-effort). Safe to call
/// when no session is active.
///
public static void ForceLogout()
{
_gate.Wait();
try { ForceLogoutLocked(); }
finally { _gate.Release(); }
}
// ---- Internals (must be called only while _gate is held) ------------
private static void EnsureFreshSessionLocked()
{
var idle = DateTime.UtcNow - _lastUsedUtc;
if (!string.IsNullOrEmpty(_cookie) && idle < _idleTimeout)
{
// Active cookie within idle window — reuse it.
return;
}
// Either no cookie, or cookie is stale. Close any existing session first
// (best-effort) so we don't leak it before opening a new one.
if (!string.IsNullOrEmpty(_cookie))
{
try { PostLogoutXml(); } catch { /* swallow — logging out a stale cookie is best-effort */ }
_cookie = string.Empty;
}
DoLoginLocked();
}
private static void DoLoginLocked()
{
using (var client = BuildAuthenticatedClient(out HttpResponseMessage _))
{
var content = new StringContent(BuildLoginXml(), Encoding.UTF8, "application/xml");
var response = client.PostAsync(_loginURL, content).Result;
var responseContent = response.Content.ReadAsStringAsync().Result;
// Capture session cookie from Set-Cookie header if present.
if (response.Headers.Contains("Set-Cookie"))
{
foreach (var c in response.Headers.GetValues("Set-Cookie")) _cookie = c;
}
var txState = ReadTxState(responseContent);
if (string.Equals(txState, "MAX_SESSIONS_IN_USE", StringComparison.OrdinalIgnoreCase))
{
_cookie = string.Empty;
throw new MaxSessionsInUseException();
}
if (string.IsNullOrEmpty(_cookie))
{
throw new InvalidOperationException("eNatis login returned no JSESSIONID cookie. TxState=" + (txState ?? ""));
}
_lastUsedUtc = DateTime.UtcNow;
}
}
private static void ForceLogoutLocked()
{
if (string.IsNullOrEmpty(_cookie)) return;
try { PostLogoutXml(); }
catch { /* swallow — logout is best-effort, we always clear the cookie below */ }
_cookie = string.Empty;
_lastUsedUtc = DateTime.MinValue;
}
private static void PostLogoutXml()
{
var handler = BuildHandler(addCookie: true);
using (var client = new HttpClient(handler))
{
AddBasicAuth(client);
var content = new StringContent(BuildLogoutXml(), Encoding.UTF8, "application/xml");
client.PostAsync(_loginURL, content).Wait();
}
}
private static HttpClient BuildAuthenticatedClient(out HttpResponseMessage _)
{
_ = null;
var handler = BuildHandler(addCookie: false);
var client = new HttpClient(handler);
AddBasicAuth(client);
return client;
}
private static WebRequestHandler BuildHandler(bool addCookie)
{
var handler = new WebRequestHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual
};
// Load eNatis client cert.
string certificatePath = ConfigurationManager.AppSettings["eNatisCertificatePath"];
string certificatePassword = ConfigurationManager.AppSettings["eNatisCertificatePassword"];
if (string.IsNullOrEmpty(certificatePath))
throw new InvalidOperationException("eNatisCertificatePath is not configured in web.config");
if (!File.Exists(certificatePath))
throw new InvalidOperationException("eNatis certificate file not found at: " + certificatePath);
var cert = new X509Certificate2(
certificatePath,
certificatePassword,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
handler.ClientCertificates.Add(cert);
// TLS 1.1/1.2 — eNatis still negotiates older suites.
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11;
ServicePointManager.ServerCertificateValidationCallback = (s, c, ch, e) => true;
if (addCookie && !string.IsNullOrEmpty(_cookie))
{
var jar = new CookieContainer();
try
{
int eq = _cookie.IndexOf("=");
int semi = _cookie.IndexOf(";", eq + 1);
if (semi == -1) semi = _cookie.Length;
string value = _cookie.Substring(eq + 1, semi - eq - 1);
jar.Add(new Uri(_loginURL), new Cookie("JSESSIONID", value));
}
catch { /* malformed cookie — let eNatis complain */ }
handler.CookieContainer = jar;
}
return handler;
}
private static void AddBasicAuth(HttpClient client)
{
var user = ConfigurationManager.AppSettings["eNatisUsername"];
var pass = ConfigurationManager.AppSettings["eNatisPassword"];
var creds = Convert.ToBase64String(Encoding.ASCII.GetBytes(user + ":" + pass));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", creds);
}
private static string BuildLoginXml()
{
return BuildEnvelope(ConfigurationManager.AppSettings["eNatisTxanType"]);
}
private static string BuildLogoutXml()
{
return BuildEnvelope("CLOSE");
}
private static string BuildEnvelope(string txanType)
{
var sb = new StringBuilder();
sb.Append("");
sb.Append("");
sb.Append("").Append(ConfigurationManager.AppSettings["eNatisOpModInd"]).Append("");
sb.Append("").Append(ConfigurationManager.AppSettings["eNatisESTxanID"]).Append("");
sb.Append("").Append(ConfigurationManager.AppSettings["eNatisESUserN"]).Append("");
sb.Append("").Append(ConfigurationManager.AppSettings["eNatisESUID"]).Append("");
sb.Append("").Append(ConfigurationManager.AppSettings["eNatisEStermID"]).Append("");
sb.Append("");
sb.Append("").Append(ConfigurationManager.AppSettings["eNatisUsername"]).Append("");
sb.Append("").Append(ConfigurationManager.AppSettings["eNatisPassword"]).Append("");
sb.Append("").Append(txanType).Append("");
sb.Append("");
return sb.ToString();
}
private static string ReadTxState(string xml)
{
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
var node = doc.SelectSingleNode("//TxState/Value") ?? doc.SelectSingleNode("//TxState");
return node != null ? node.InnerText : null;
}
catch { return null; }
}
}
public class MaxSessionsInUseException : Exception
{
public MaxSessionsInUseException() : base("eNatis returned MAX_SESSIONS_IN_USE.") { }
}
}