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.") { } } }