using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; using System.Threading; namespace Creditcall.ChipDna.Client { class ClientApp { private const int ErrorExit = 1; private const int SuccessExit = 0; private const string ConfigFilename = "client.config.xml"; private const int DefaultPort = 1869; private const string DefaultIpAddress = "127.0.0.1"; private const bool DefaultSaveReceipt = false; private const int transactionTimeoutSeconds = 300; private static Client client; private static int Main() { try { var settings = new ConfigFileParser(GetAbsolutePath(ConfigFilename)); string tid = settings.TerminalId; string apiKey = settings.ApiKey; string posId = settings.PosId; string address = settings.ConnectAddress ?? DefaultIpAddress; string sslHost = settings.SslHostName; bool saveReceipt = settings.SaveReceipt ?? DefaultSaveReceipt; int port = DefaultPort; if (!string.IsNullOrEmpty(settings.ConnectionPort)) { if (!int.TryParse(settings.ConnectionPort, out port)) { Console.WriteLine("Invalid port number in config."); return ErrorExit; } } string identifier = !string.IsNullOrEmpty(tid) ? tid : !string.IsNullOrEmpty(posId) ? posId : null; if (identifier == null || (string.IsNullOrEmpty(apiKey) && string.IsNullOrEmpty(tid))) { Console.WriteLine("Missing valid credentials."); return ErrorExit; } Console.WriteLine($"Starting ChipDNAClient with ID={identifier}, Address={address}:{port}"); client = new Client(identifier, address, port, sslHost, saveReceipt, settings); StartHttpServer(); return SuccessExit; } catch (Exception ex) { Console.WriteLine("Startup failed: " + ex.Message); return ErrorExit; } } private static void StartHttpServer() { HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://127.0.0.1:18181/start-transaction/"); listener.Prefixes.Add("http://127.0.0.1:18181/confirm-transaction/"); listener.Prefixes.Add("http://127.0.0.1:18181/transaction-information/"); listener.Prefixes.Add("http://127.0.0.1:18181/void-transaction/"); listener.Prefixes.Add("http://127.0.0.1:18181/chipdna-status/"); listener.Start(); Console.WriteLine("Listening on:"); Console.WriteLine(" http://127.0.0.1:18181/start-transaction/"); Console.WriteLine(" http://127.0.0.1:18181/confirm-transaction/"); Console.WriteLine(" http://127.0.0.1:18181/transaction-information/"); Console.WriteLine(" http://127.0.0.1:18181/void-transaction/"); Console.WriteLine(" http://127.0.0.1:18181/chipdna-status/"); while (true) { HttpListenerContext context = listener.GetContext(); HttpListenerRequest request = context.Request; string rawUrl = request.Url.AbsolutePath; try { if (request.HttpMethod != "POST") { context.Response.StatusCode = 405; context.Response.Close(); continue; } if (rawUrl.StartsWith("/start-transaction", StringComparison.OrdinalIgnoreCase)) { HandleStartTransaction(context); } else if (rawUrl.StartsWith("/confirm-transaction", StringComparison.OrdinalIgnoreCase)) { HandleConfirmTransaction(context); } else if (rawUrl.StartsWith("/transaction-information", StringComparison.OrdinalIgnoreCase)) { HandleTransactionInformation(context); } else if (rawUrl.StartsWith("/void-transaction", StringComparison.OrdinalIgnoreCase)) { HandleVoidTransaction(context); } else if (rawUrl.StartsWith("/chipdna-status", StringComparison.OrdinalIgnoreCase)) { HandleGetStatus(context); } else { context.Response.StatusCode = 404; context.Response.Close(); } } catch (Exception ex) { Console.WriteLine("Unhandled server error: " + ex.Message); try { context.Response.StatusCode = 500; context.Response.Close(); } catch { } } } } private static void HandleGetStatus(HttpListenerContext context) { try { var statusResult = client.GetStatus(); var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList)); context.Response.ContentType = "application/xml"; var wrappedResult = new SerializableKeyValueList(statusResult); responseSerializer.Serialize(context.Response.OutputStream, wrappedResult); } catch (Exception ex) { Console.WriteLine("Error in -status: " + ex.Message); context.Response.StatusCode = 500; } finally { context.Response.OutputStream.Close(); } } private static void HandleTransactionInformation(HttpListenerContext context) { try { var serializer = new XmlSerializer(typeof(TransactionReferenceRequest)); TransactionReferenceRequest req = (TransactionReferenceRequest)serializer.Deserialize(context.Request.InputStream); var transactionResult = client.PerformGetTransactionInformation(req.TransactionReference); // Return the final key/value list var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList)); context.Response.ContentType = "application/xml"; var wrappedResult = new SerializableKeyValueList(transactionResult); responseSerializer.Serialize(context.Response.OutputStream, wrappedResult); } catch (Exception ex) { Console.WriteLine("Error in transaction-information: " + ex.Message); context.Response.StatusCode = 500; } finally { context.Response.OutputStream.Close(); } } private static void HandleVoidTransaction(HttpListenerContext context) { try { var serializer = new XmlSerializer(typeof(TransactionReferenceRequest)); TransactionReferenceRequest req = (TransactionReferenceRequest)serializer.Deserialize(context.Request.InputStream); var transactionResult = client.PerformVoidTransaction(req.TransactionReference); // Return the final key/value list var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList)); context.Response.ContentType = "application/xml"; var wrappedResult = new SerializableKeyValueList(transactionResult); responseSerializer.Serialize(context.Response.OutputStream, wrappedResult); } catch (Exception ex) { Console.WriteLine("Error in transaction-information: " + ex.Message); context.Response.StatusCode = 500; } finally { context.Response.OutputStream.Close(); } } private static void HandleConfirmTransaction(HttpListenerContext context) { try { var serializer = new XmlSerializer(typeof(ConfirmTransactionRequest)); ConfirmTransactionRequest payload = (ConfirmTransactionRequest)serializer.Deserialize(context.Request.InputStream); Console.WriteLine($"Received confirm transaction: Amount={payload.Amount}, TransactionReference={payload.TransactionReference}"); var confirmationResult = client.PerformConfirmTransaction(payload.TransactionReference, payload.Amount); var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList)); context.Response.ContentType = "application/xml"; var wrappedResult = new SerializableKeyValueList(confirmationResult); responseSerializer.Serialize(context.Response.OutputStream, wrappedResult); } catch (Exception ex) { var errorMap = new Dictionary { { ParameterKeys.TransactionResult, "ERROR" }, { ParameterKeys.Errors, $"ConfirmTransaction failed: {ex.Message}" } }; var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList)); context.Response.ContentType = "application/xml"; var wrappedResult = new SerializableKeyValueList(errorMap); responseSerializer.Serialize(context.Response.OutputStream, wrappedResult); } finally { context.Response.OutputStream.Close(); } } private static void HandleStartTransaction(HttpListenerContext context) { try { var serializer = new XmlSerializer(typeof(TransactionPayload)); TransactionPayload payload = (TransactionPayload)serializer.Deserialize(context.Request.InputStream); Console.WriteLine($"Received transaction request: Amount={payload.Amount}, Type={payload.TransactionType}"); client.TransactionCompletionSource = new TaskCompletionSource>(); // start transaction (non-blocking) client.PerformStartTransaction(payload.Amount, payload.TransactionType); // Wait for completion with timeout var task = client.TransactionCompletionSource.Task; if (!task.Wait(TimeSpan.FromSeconds(transactionTimeoutSeconds))) { // timeout var errorMap = new Dictionary { { ParameterKeys.TransactionResult, "TIMEOUT" }, { ParameterKeys.Errors, "Transaction timed out waiting for device response" } }; var wrappedTimeout = new SerializableKeyValueList(errorMap); context.Response.ContentType = "application/xml"; new XmlSerializer(typeof(SerializableKeyValueList)).Serialize(context.Response.OutputStream, wrappedTimeout); return; } // Task completed — handle possible exception Dictionary transactionResult; try { transactionResult = task.Result; // safe now, Wait returned } catch (AggregateException agg) { var first = agg.Flatten().InnerExceptions[0]; var errMap = new Dictionary { { ParameterKeys.TransactionResult, "ERROR" }, { ParameterKeys.Errors, $"Transaction task faulted: {first.Message}" } }; var wrapped = new SerializableKeyValueList(errMap); context.Response.ContentType = "application/xml"; new XmlSerializer(typeof(SerializableKeyValueList)).Serialize(context.Response.OutputStream, wrapped); return; } var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList)); context.Response.ContentType = "application/xml"; var wrappedResult = new SerializableKeyValueList(transactionResult); responseSerializer.Serialize(context.Response.OutputStream, wrappedResult); } catch (Exception ex) { Console.WriteLine("Error in start-transaction: " + ex.Message); context.Response.StatusCode = 500; } finally { context.Response.OutputStream.Close(); } } private static string GetAbsolutePath(string fileName) { var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); return Path.Combine(path ?? "", fileName); } private static string EscapeForJson(string input) { return input?.Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\n", "\\n") .Replace("\r", "\\r"); } private static string SerializeToJson(Dictionary dict) { var sb = new StringBuilder(); sb.Append("{"); foreach (var kv in dict) { sb.Append($"\"{EscapeForJson(kv.Key)}\":\"{EscapeForJson(kv.Value)}\","); } if (sb.Length > 1) sb.Length--; // remove last comma sb.Append("}"); return sb.ToString(); } // helper to serialize an object to an XML string (no namespaces) private static string SerializeToXml(T obj, bool omitXmlDeclaration = true) { if (obj == null) return string.Empty; var xs = new XmlSerializer(typeof(T)); var ns = new XmlSerializerNamespaces(); ns.Add(string.Empty, string.Empty); // remove xsi/xsd namespaces var settings = new XmlWriterSettings { OmitXmlDeclaration = omitXmlDeclaration, Indent = false, Encoding = Encoding.UTF8 }; using (var sw = new StringWriter()) using (var xw = XmlWriter.Create(sw, settings)) { xs.Serialize(xw, obj, ns); return sw.ToString(); } } } [XmlRoot("TransactionResult")] public class SerializableKeyValueList { [XmlElement("Entry")] public List Items { get; set; } public SerializableKeyValueList() { } public SerializableKeyValueList(Dictionary dict) { Items = new List(); foreach (var kv in dict) { Items.Add(new Entry { Key = kv.Key, Value = kv.Value }); } } } public class Entry { [XmlElement("Key")] public string Key { get; set; } [XmlElement("Value")] public string Value { get; set; } } [XmlRoot("TransactionPayload")] public class TransactionPayload { public string Amount { get; set; } public string TransactionType { get; set; } } [XmlRoot("TransactionConfirmation")] public class TransactionConfirmation { public string Result { get; set; } public string Errors { get; set; } public string ErrorDescription { get; set; } public string ReceiptDataCardholder { get; set; } } [XmlRoot("TransactionReferenceRequest")] public class TransactionReferenceRequest { [XmlElement("TransactionReference")] public string TransactionReference { get; set; } } [XmlRoot("ConfirmTransactionRequest")] public class ConfirmTransactionRequest { public string Amount { get; set; } public string TransactionReference { get; set; } } }