446 lines
17 KiB
C#
446 lines
17 KiB
C#
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-stream/");
|
|
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-stream/");
|
|
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-stream", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
HandleStartTransactionStream(context);
|
|
}
|
|
else 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<string, string>
|
|
{
|
|
{ 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}");
|
|
|
|
var transactionResult = ExecuteTransaction(payload);
|
|
|
|
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 Dictionary<string, string> ExecuteTransaction(TransactionPayload payload)
|
|
{
|
|
client.TransactionCompletionSource = new TaskCompletionSource<Dictionary<string, string>>();
|
|
return TransactionExecution.Run(
|
|
() => client.PerformStartTransaction(payload.Amount, payload.TransactionType),
|
|
() => client.TransactionCompletionSource.Task,
|
|
client.SuppressUncertainProgress,
|
|
TimeSpan.FromSeconds(transactionTimeoutSeconds));
|
|
}
|
|
|
|
private static void HandleStartTransactionStream(HttpListenerContext context)
|
|
{
|
|
context.Response.ContentType = "application/x-ndjson; charset=utf-8";
|
|
context.Response.SendChunked = true;
|
|
var writer = new TransactionStreamWriter(context.Response.OutputStream);
|
|
var delivery = Task.Run(() => writer.Deliver());
|
|
Action<string, string> progress = writer.Progress;
|
|
try
|
|
{
|
|
TransactionPayload payload;
|
|
using (var reader = new StreamReader(context.Request.InputStream))
|
|
using (var json = new Newtonsoft.Json.JsonTextReader(reader))
|
|
{
|
|
payload = new Newtonsoft.Json.JsonSerializer().Deserialize<TransactionPayload>(json);
|
|
}
|
|
if (payload == null)
|
|
throw new InvalidDataException("Missing transaction payload");
|
|
|
|
var result = client.Progress.Observe(progress, () => ExecuteTransaction(payload));
|
|
writer.Complete(new { type = "result", result = TransactionStreamFields.Required(result) });
|
|
}
|
|
catch (Exception)
|
|
{
|
|
writer.Complete(new { type = "error", error = "Transaction stream failed" });
|
|
}
|
|
finally
|
|
{
|
|
// Retain serial response completion, as with the XML endpoint.
|
|
// Hardlink's existing per-call timeout bounds its connection;
|
|
// no new delivery timer may truncate an approved final result.
|
|
try { delivery.GetAwaiter().GetResult(); }
|
|
finally
|
|
{
|
|
try { context.Response.Close(); } catch (HttpListenerException) { }
|
|
}
|
|
}
|
|
}
|
|
|
|
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<string, string> 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>(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<Entry> Items { get; set; }
|
|
|
|
public SerializableKeyValueList() { }
|
|
|
|
public SerializableKeyValueList(Dictionary<string, string> dict)
|
|
{
|
|
Items = new List<Entry>();
|
|
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; }
|
|
}
|
|
|
|
}
|