Update AfRApay.FTM to new APIs & cleanup code
This commit is contained in:
parent
67e18ee192
commit
8a87c258b8
|
@ -3,7 +3,7 @@ using PCSC;
|
|||
using PCSC.Monitoring;
|
||||
using PCSC.Iso7816;
|
||||
|
||||
const decimal DefaultAmount = 1.50M;
|
||||
const int defaultAmount = 150;
|
||||
|
||||
var rootCommand = new RootCommand("Fancy Test Machine for AfRApay");
|
||||
|
||||
|
@ -15,119 +15,121 @@ webAddrOption.SetDefaultValue(new Uri("http://127.0.0.1:5296"));
|
|||
rootCommand.Add(webAddrOption);
|
||||
|
||||
rootCommand.SetHandler((listReaders, webAddr) => {
|
||||
using (var context = ContextFactory.Instance.Establish(SCardScope.System)) {
|
||||
// Ignore Yubikeys.
|
||||
var readerNames = context.GetReaders()
|
||||
.Where((readerName) => !readerName.Contains("Yubico"))
|
||||
.ToArray();
|
||||
using var context = ContextFactory.Instance.Establish(SCardScope.System);
|
||||
// Ignore Yubikeys.
|
||||
var readerNames = context.GetReaders()
|
||||
.Where((readerName) => !readerName.Contains("Yubico"))
|
||||
.ToArray();
|
||||
|
||||
// We need at least one card reader or this won't work!
|
||||
if (readerNames.Length == 0) {
|
||||
Console.Error.WriteLine("Error: no card reader detected");
|
||||
Environment.Exit(1);
|
||||
// We need at least one card reader or this won't work!
|
||||
if (readerNames.Length == 0) {
|
||||
Console.Error.WriteLine("Error: no card reader detected");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
// If --list-readers is passed, list readers and exit.
|
||||
if (listReaders) {
|
||||
Console.Error.WriteLine("----------- Connected Readers ----------");
|
||||
foreach (var name in readerNames) {
|
||||
Console.WriteLine(name);
|
||||
}
|
||||
Console.Error.WriteLine("----------------------------------------");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
// If --list-readers is passed, list readers and exit.
|
||||
if (listReaders) {
|
||||
Console.Error.WriteLine("----------- Connected Readers ----------");
|
||||
foreach (var name in readerNames) {
|
||||
Console.WriteLine(name);
|
||||
}
|
||||
Console.Error.WriteLine("----------------------------------------");
|
||||
Environment.Exit(0);
|
||||
// Setup!
|
||||
using HttpClient httpClient = new();
|
||||
httpClient.BaseAddress = webAddr;
|
||||
|
||||
var state = TerminalState.Debit;
|
||||
decimal amount = defaultAmount;
|
||||
|
||||
Console.Error.WriteLine("----------------------------------------");
|
||||
Console.Error.WriteLine("--- AfRApay FTM - Fancy Test Machine ---");
|
||||
Console.Error.WriteLine("----------------------------------------");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("AfRApay.Web: {0}", httpClient.BaseAddress);
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Hotkeys (case insensitive):");
|
||||
Console.Error.WriteLine(" [-] Debit (default)");
|
||||
Console.Error.WriteLine(" [+] Credit");
|
||||
Console.Error.WriteLine(" [=] Set amount (default: €{0:C})", amount/100M);
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine(" [B] Balance query");
|
||||
Console.Error.WriteLine(" [L] Link card (initiate from web UI)");
|
||||
Console.Error.WriteLine(" [Esc] Cancel, reset state and amount");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("----------------------------------------");
|
||||
|
||||
// Listen for events on all connected readers.
|
||||
using var monitor = MonitorFactory.Instance.Create(SCardScope.System);
|
||||
monitor.Initialized += (_, args) => Console.WriteLine("[ Reader Initialized: {0} ]", args.ReaderName);
|
||||
monitor.MonitorException += (_, args) => {
|
||||
Console.Error.WriteLine("! ERROR: {0}", args);
|
||||
Environment.Exit(1);
|
||||
};
|
||||
monitor.StatusChanged += (_, args) => Console.WriteLine("~ {0} -> {1}", args.LastState, args.NewState);
|
||||
monitor.CardInserted += (_, args) => {
|
||||
Console.WriteLine("> TAP: {0}", Convert.ToHexString(args.Atr));
|
||||
var reader = new IsoReader(context, args.ReaderName, SCardShareMode.Shared, SCardProtocol.Any);
|
||||
HandleTap(reader, httpClient, state, amount);
|
||||
};
|
||||
monitor.CardRemoved += (_, _) => {
|
||||
Console.WriteLine("< OFF");
|
||||
Console.WriteLine(); // Write a blank line between card taps for readability.
|
||||
};
|
||||
Console.WriteLine("[ Starting... ]");
|
||||
monitor.Start(readerNames);
|
||||
|
||||
// Handle hotkeys.
|
||||
while (true) {
|
||||
var key = Console.ReadKey();
|
||||
var dontPrint = false;
|
||||
switch (key.Key) {
|
||||
case ConsoleKey.Subtract:
|
||||
case ConsoleKey.OemMinus:
|
||||
state = TerminalState.Debit;
|
||||
break;
|
||||
case ConsoleKey.Add:
|
||||
case ConsoleKey.OemPlus:
|
||||
state = TerminalState.Credit;
|
||||
break;
|
||||
case 0 when key.KeyChar == '=':
|
||||
Console.Error.Write("\b => ENTER AMOUNT: ");
|
||||
amount = Math.Abs(int.Parse(Console.ReadLine() ?? "150".Trim()));
|
||||
break;
|
||||
case ConsoleKey.L:
|
||||
state = TerminalState.Link;
|
||||
break;
|
||||
case ConsoleKey.B:
|
||||
state = TerminalState.Balance;
|
||||
break;
|
||||
case ConsoleKey.Escape:
|
||||
state = TerminalState.Debit;
|
||||
amount = defaultAmount;
|
||||
break;
|
||||
default:
|
||||
Console.Error.Write("\b");
|
||||
dontPrint = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Setup!
|
||||
using HttpClient httpClient = new();
|
||||
httpClient.BaseAddress = webAddr;
|
||||
|
||||
var state = TerminalState.Debit;
|
||||
decimal amount = DefaultAmount;
|
||||
|
||||
Console.Error.WriteLine("----------------------------------------");
|
||||
Console.Error.WriteLine("--- AfRApay FTM - Fancy Test Machine ---");
|
||||
Console.Error.WriteLine("----------------------------------------");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("AfRApay.Web: {0}", httpClient.BaseAddress);
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Hotkeys (case insensitive):");
|
||||
Console.Error.WriteLine(" [-] Debit (default)");
|
||||
Console.Error.WriteLine(" [+] Credit");
|
||||
Console.Error.WriteLine(" [=] Set amount (default: €{0})", amount);
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine(" [B] Balance query");
|
||||
Console.Error.WriteLine(" [L] Link card (initiate from web UI)");
|
||||
Console.Error.WriteLine(" [Esc] Cancel, reset state and amount");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("----------------------------------------");
|
||||
|
||||
// Listen for events on all connected readers.
|
||||
using var monitor = MonitorFactory.Instance.Create(SCardScope.System);
|
||||
monitor.Initialized += (_, args) => Console.WriteLine("[ Reader Initialized: {0} ]", args.ReaderName);
|
||||
monitor.MonitorException += (_, args) => {
|
||||
Console.Error.WriteLine("! ERROR: {0}", args);
|
||||
Environment.Exit(1);
|
||||
};
|
||||
monitor.StatusChanged += (_, args) => Console.WriteLine("~ {0} -> {1}", args.LastState, args.NewState);
|
||||
monitor.CardInserted += (_, args) => {
|
||||
Console.WriteLine("> TAP: {0}", Convert.ToHexString(args.Atr));
|
||||
var reader = new IsoReader(context, args.ReaderName, SCardShareMode.Shared, SCardProtocol.Any);
|
||||
HandleTap(reader, httpClient, state, amount);
|
||||
};
|
||||
monitor.CardRemoved += (_, args) => {
|
||||
Console.WriteLine("< OFF");
|
||||
Console.WriteLine(); // Write a blank line between card taps for readability.
|
||||
};
|
||||
Console.WriteLine("[ Starting... ]");
|
||||
monitor.Start(readerNames);
|
||||
|
||||
// Handle hotkeys.
|
||||
while (true) {
|
||||
var key = Console.ReadKey();
|
||||
var dontPrint = false;
|
||||
switch (key.Key) {
|
||||
case ConsoleKey.Subtract:
|
||||
case ConsoleKey.OemMinus:
|
||||
state = TerminalState.Debit;
|
||||
break;
|
||||
case ConsoleKey.Add:
|
||||
case ConsoleKey.OemPlus:
|
||||
state = TerminalState.Credit;
|
||||
break;
|
||||
case (ConsoleKey)0 when key.KeyChar == '=':
|
||||
Console.Error.Write("\b => ENTER AMOUNT: ");
|
||||
amount = Math.Abs(Decimal.Parse(Console.ReadLine() ?? "1.50".Trim()));
|
||||
break;
|
||||
case ConsoleKey.L:
|
||||
state = TerminalState.Link;
|
||||
break;
|
||||
case ConsoleKey.B:
|
||||
state = TerminalState.Balance;
|
||||
break;
|
||||
case ConsoleKey.Escape:
|
||||
state = TerminalState.Debit;
|
||||
amount = DefaultAmount;
|
||||
break;
|
||||
default:
|
||||
Console.Error.Write("\b");
|
||||
dontPrint = true;
|
||||
break;
|
||||
};
|
||||
if (dontPrint) {
|
||||
// Invalid input, just ignore it.
|
||||
} else if (state == TerminalState.Debit || state == TerminalState.Credit) {
|
||||
Console.Error.WriteLine("\b => {0}: €{1}", state, amount);
|
||||
} else {
|
||||
Console.Error.WriteLine("\b => {0}", state);
|
||||
}
|
||||
|
||||
if (dontPrint) {
|
||||
// Invalid input, just ignore it.
|
||||
} else if (state is TerminalState.Debit or TerminalState.Credit) {
|
||||
Console.Error.WriteLine("\b => {0}: €{1}", state, amount);
|
||||
} else {
|
||||
Console.Error.WriteLine("\b => {0}", state);
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once FunctionNeverReturns
|
||||
}, listReadersOption, webAddrOption);
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
|
||||
// Queries a card for data when one is tapped.
|
||||
static async void HandleTap(IsoReader reader, HttpClient httpClient, TerminalState state, decimal amount) {
|
||||
static async void HandleTap(IIsoReader reader, HttpClient httpClient, TerminalState state, decimal amount) {
|
||||
// Send a PCSC pseudo-APDU to query the ISO 14443 UID.
|
||||
var uidRsp = reader.Transmit(new CommandApdu(IsoCase.Case2Short, SCardProtocol.Any) {
|
||||
CLA = 0xFF,
|
||||
|
@ -149,14 +151,14 @@ static async void HandleTap(IsoReader reader, HttpClient httpClient, TerminalSta
|
|||
var finalAmount = Math.Abs(amount) * (state == TerminalState.Debit ? 1 : -1);
|
||||
var idempotencyKey = new byte[18];
|
||||
Random.Shared.NextBytes(idempotencyKey);
|
||||
await CallGet(httpClient, String.Format("/api/card/transaction?card={0}&amount={1}&ik={2}",
|
||||
Convert.ToHexString(uid), finalAmount, Uri.EscapeDataString(Convert.ToBase64String(idempotencyKey))));
|
||||
var ik = Convert.ToHexString(idempotencyKey);
|
||||
await CallPut(httpClient, $"/api/card/{Convert.ToHexString(uid)}/transaction/{ik}?amount={finalAmount}");
|
||||
break;
|
||||
case TerminalState.Link:
|
||||
await CallGet(httpClient, String.Format("/api/card/link?card={0}", Convert.ToHexString(uid)));
|
||||
await CallGet(httpClient, $"/api/card/{Convert.ToHexString(uid)}/link");
|
||||
break;
|
||||
case TerminalState.Balance:
|
||||
await CallGet(httpClient, String.Format("/api/card/balance?card={0}", Convert.ToHexString(uid)));
|
||||
await CallGet(httpClient, $"/api/card/{Convert.ToHexString(uid)}/balance");
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine("UNKNOWN TERMINAL STATE: {0}", state);
|
||||
|
@ -166,7 +168,7 @@ static async void HandleTap(IsoReader reader, HttpClient httpClient, TerminalSta
|
|||
|
||||
// Was the command successful?
|
||||
static bool IsSucc(Response rsp) {
|
||||
return rsp.SW1 == (byte)SW1Code.Normal && rsp.SW2 == 0x00;
|
||||
return rsp is { SW1: (byte)SW1Code.Normal, SW2: 0x00 };
|
||||
}
|
||||
|
||||
static async Task<string> CallGet(HttpClient client, string path) {
|
||||
|
@ -176,8 +178,15 @@ static async Task<string> CallGet(HttpClient client, string path) {
|
|||
return rsp;
|
||||
}
|
||||
|
||||
static async Task<string> CallPut(HttpClient client, string path) {
|
||||
Console.WriteLine(" -> PUT {0}", path);
|
||||
var rsp = await client.PutAsync(path, new StringContent("")).Result.Content.ReadAsStringAsync();
|
||||
Console.WriteLine(" <- {0}", rsp);
|
||||
return rsp;
|
||||
}
|
||||
|
||||
// Terminal State.
|
||||
enum TerminalState {
|
||||
internal enum TerminalState {
|
||||
Debit,
|
||||
Credit,
|
||||
Link,
|
||||
|
|
Loading…
Reference in a new issue