| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- using ExtensionMethods;
- using MAX.Models;
- using Microsoft.Extensions.Logging;
- using System;
- using System.Net.Sockets;
- using System.Security.Cryptography;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Xml;
- namespace MAX
- {
- public class Client : IDisposable
- {
- private ILogger _logger;
- private string _host;
- private int _port;
- private int _vendorId;
- private string _serialNumber;
- private int _userId;
- private string _username;
- private string _password;
- private bool _logResponses;
- private TcpClient _connection = null;
- private NetworkStream _connectionStream = null;
- private TripleDES _des = null;
- private bool _disposed = false;
- public Client(ILogger logger, bool logResponses, string host, int port, int vendorId, string serialNumber, int userId, string username, string password)
- {
- _logger = logger;
- _host = host;
- _port = port;
- _vendorId = vendorId;
- _serialNumber = serialNumber;
- _userId = userId;
- _username = username;
- _password = password;
- _logResponses = logResponses;
- ConnectTimeout = 10000;
- ReceiveTimeout = 10000;
- SendTimeout = 10000;
- }
- public Client(ILogger logger, bool logResponses, string host, int port, LoginCredentials credentials)
- : this(logger, logResponses, host, port, credentials.Vendor.Id, credentials.Vendor.SerialNumber,
- credentials.User.Id, credentials.User.Username, credentials.Password)
- {
- }
- public void Close()
- {
- Dispose(true);
- }
- public async Task<User> ConnectAsync()
- {
- if (_connection != null)
- throw new Exception("Already connected");
- _connection = new TcpClient(AddressFamily.InterNetwork);
- _connection.ReceiveTimeout = ReceiveTimeout;
- _connection.SendTimeout = SendTimeout;
- // Connect to the server
- try
- {
- using (var cancellationSource = new CancellationTokenSource(ConnectTimeout))
- {
- await _connection.ConnectAsync(_host, _port).WithCancellation(cancellationSource.Token).ConfigureAwait(false);
- }
- }
- catch (OperationCanceledException)
- {
- throw new Exception("Connect timeout");
- }
- _connectionStream = _connection.GetStream();
- // Device authentication
- await WriteMessageAsync(new MessageBuilder()
- .Append("Hi ")
- .Append(_serialNumber)
- .Append("|V")
- .Append(_vendorId)
- .Append("|123451234512345||||||")).ConfigureAwait(false);
- var response = ExpectResponse(await ReadMessageAsync().ConfigureAwait(false), "Hi");
- // Request server RSA key
- //
- // WARNING:
- //
- // The protocol does not do any form of server authentication so this step is
- // vulnerable to a man-in-the-middle attack where an intermediary intercepts this
- // request and sends their own RSA key while keeping the server RSA key to themselves.
- // This is not really an issue here as this is server to server communication and
- // therefore less likely to be intercepted.
- await WriteMessageAsync(new MessageBuilder().Append("PK")).ConfigureAwait(false);
- response = await ReadMessageAsync().ConfigureAwait(false);
- // Key exchange
- _des = TripleDES.Create();
- _des.IV = new byte[8];
- if (_logResponses)
- {
- _logger.LogDebug("Key for {0}: {1}",
- LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber),
- BitConverter.ToString(_des.Key).Replace("-", "")
- );
- }
- await WriteMessageAsync(new MessageBuilder()
- .Append("3D ")
- .Append(EncryptRSA(response, BitConverter.ToString(_des.Key).Replace("-", "")))).ConfigureAwait(false);
- response = ExpectResponse(await ReadMessageAsync().ConfigureAwait(false), "OK");
- // User authentication
- await WriteMessageAsync(new MessageBuilder()
- .Append("User ")
- .Append(Encrypt(new StringBuilder()
- .Append(_userId)
- .Append("|")
- .Append(_username)
- .Append("|")
- .Append(_password).ToString()))).ConfigureAwait(false);
- response = ExpectResponse(Decrypt(await ReadMessageAsync().ConfigureAwait(false)), "OK");
- var parts = response.Split('|');
- var user = new User()
- {
- Id = _userId,
- Username = _username,
- FirstName = parts[4],
- Surname = parts[3],
- Enabled = ParseBool(parts[6], "User.Enabled(6)", response),
- Level = (User.UserLevel)ParseInt(parts[1], "User.Level(1)", response),
- System = ParseInt(parts[2], "User.System(2)", response),
- LastLogin = ParseDateTime(parts[5], "User.LastLogin(5)", response)
- };
- if (user.Level == User.UserLevel.CustomUser)
- {
- user.CanPrintOffline = ParseBool(parts[7], "User.CanPrintOffline(7)", response);
- user.OfflinePrintValue = ParseDecimal(parts[8], "User.OfflinePrintValue(8)", response);
- user.CanPrintOnline = ParseBool(parts[9], "User.CanPrintOnline(9)", response);
- user.OnlinePrintValue = ParseDecimal(parts[10], "User.OnlinePrintValue(10)", response);
- user.CanReprintOffline = ParseBool(parts[11], "User.CanReprintOffline(11)", response);
- user.OfflineReprintValue = ParseDecimal(parts[12], "User.OfflineReprintValue(12)", response);
- user.CanReprintOnline = ParseBool(parts[13], "User.CanReprintOnline(13)", response);
- user.OnlineReprintValue = ParseDecimal(parts[14], "User.OnlineReprintValue(14)", response);
- user.BulkExport = ParseBool(parts[15], "User.BulkExport(15)", response);
- user.BulkExportMaxValue = ParseDecimal(parts[16], "User.BulkExportMaxValue(16)", response);
- user.BulkOrder = ParseBool(parts[17], "User.BulkOrder(17)", response);
- user.BulkOrderMaxValue = ParseDecimal(parts[18], "User.BulkOrderMaxValue(18)", response);
- user.BulkViewPins = ParseBool(parts[19], "User.BulkViewPins(19)", response);
- user.BulkReExport = ParseBool(parts[20], "User.BulkReExport(20)", response);
- }
- return user;
- }
- public int ConnectTimeout { get; set; }
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- return;
- _disposed = true;
- // No unmanaged resources are disposed so we don't need the full finalisation pattern.
- if (disposing)
- {
- if (_des != null)
- {
- _des.Dispose();
- _des = null;
- }
- if (_connectionStream != null)
- {
- _connectionStream.Dispose();
- _connectionStream = null;
- }
- if (_connection != null)
- {
- _connection.Dispose();
- _connection = null;
- }
- }
- }
- public void Dispose()
- {
- Dispose(true);
- }
- private string Decrypt(string cipherText)
- {
- var response = Utils.TripleDESDecrypt(cipherText, _des);
- if (_logResponses)
- {
- _logger.LogDebug("Decrypted response for {0}: {1}", LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber), response);
- }
- return response;
- }
- private string Encrypt(string plainText)
- {
- return Utils.TripleDESEncrypt(plainText, _des);
- }
- private string EncryptRSA(string publicKey, string plainText)
- {
- RSAParameters parameters = new RSAParameters();
- var xml = new XmlDocument();
- xml.LoadXml(publicKey);
- if (! xml.DocumentElement.Name.Equals("RSAKeyValue"))
- throw new Exception("Invalid RSA key");
- foreach (XmlNode node in xml.DocumentElement.ChildNodes)
- {
- switch (node.Name)
- {
- case "Modulus": parameters.Modulus = Convert.FromBase64String(node.InnerText); break;
- case "Exponent": parameters.Exponent = Convert.FromBase64String(node.InnerText); break;
- case "P": parameters.P = Convert.FromBase64String(node.InnerText); break;
- case "Q": parameters.Q = Convert.FromBase64String(node.InnerText); break;
- case "DP": parameters.DP = Convert.FromBase64String(node.InnerText); break;
- case "DQ": parameters.DQ = Convert.FromBase64String(node.InnerText); break;
- case "InverseQ": parameters.InverseQ = Convert.FromBase64String(node.InnerText); break;
- case "D": parameters.D = Convert.FromBase64String(node.InnerText); break;
- }
- }
- using (var rsa = RSA.Create())
- {
- rsa.ImportParameters(parameters);
- var blockSize = rsa.KeySize / 8 - 42;
- var offset = 0;
- var input = Encoding.UTF32.GetBytes(plainText);
- StringBuilder output = new StringBuilder();
- while (offset < input.Length)
- {
- var length = input.Length - offset;
- if (length > blockSize)
- length = blockSize;
- var block = new byte[length];
- Array.Copy(input, offset, block, 0, length);
- var cipherText = rsa.Encrypt(block, RSAEncryptionPadding.OaepSHA1);
- Array.Reverse(cipherText);
- output.Append(Convert.ToBase64String(cipherText));
- offset += length;
- }
- return output.ToString();
- }
- }
- private string ExpectResponse(string response, string prefix)
- {
- if (response.StartsWith("ER"))
- {
- var parts = response.Split('|');
- int errorCode;
- if ((parts.Length < 2) || !int.TryParse(parts[1], out errorCode))
- {
- errorCode = -1;
- }
- var message = parts.Length >= 3 ? parts[2] : String.Format("Malformed server error: {0}", response);
- _logger.LogError("MAX Error for {0}: {1} (code {2})",
- LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber), message, errorCode);
- throw new MAXException(errorCode, message);
- }
- else if (!response.StartsWith(prefix))
- {
- _logger.LogError("Invalid MAX response for {0}: {1}",
- LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber),
- response);
- throw new Exception(String.Format("Invalid server response: {0}", response));
- }
- return response;
- }
- public async Task<Account> GetAccountAsync()
- {
- await WriteMessageAsync(new MessageBuilder().Append("Acc")).ConfigureAwait(false);
- var response = ExpectResponse(Decrypt(await ReadMessageAsync().ConfigureAwait(false)), "OK");
- var parts = response.Split('|');
- return new Account()
- {
- Id = ParseInt(parts[1], "Account.Id(1)", response),
- Name = parts[2],
- Balance = ParseDecimal(parts[3], "Account.Balance(3)", response),
- Status = (Account.AccountStatus)ParseInt(parts[4], "Account.AccountStatus(4)", response),
- Reference = parts[5],
- Warehouse = new Warehouse()
- {
- Id = ParseInt(parts[6], "Account.Warehouse.Id(6)", response),
- Name = parts[7]
- }
- };
- }
- public async Task<ProductCatalogue> GetProductCatalogueAsync(Account account)
- {
- var encryptedWarehouseName = Encrypt(account.Warehouse.Name);
- await WriteMessageAsync(new MessageBuilder()
- .Append("Pdt ")
- .Append(encryptedWarehouseName)).ConfigureAwait(false);
- var response = ExpectResponse(Decrypt(await ReadMessageAsync().ConfigureAwait(false)), "OK");
- var parts = response.Split('|');
- var count = ParseInt(parts[1], "Products.Count(1)", response);
- var catalogue = new ProductCatalogue();
-
- var listCommand = new MessageBuilder().Append("List ")
- .Append(encryptedWarehouseName).GetBytes();
- for (var i = 0; i < count; i++)
- {
- await _connectionStream.WriteAsync(listCommand, 0, listCommand.Length).ConfigureAwait(false);
- response = ExpectResponse(Decrypt(await ReadMessageAsync().ConfigureAwait(false)), "OK");
- parts = response.Split('|');
- int networkId = ParseInt(parts[4], "Product.NetworkId(4)", response);
- Network network;
- if (! catalogue.NetworkMap.TryGetValue(networkId, out network))
- {
- network = catalogue.AddNetwork(networkId, parts[5]);
- }
- catalogue.AddProduct(
- network: network,
- id: ParseInt(parts[1], "Product.Id(1)", response),
- faceValue: ParseDecimal(parts[2], "Product.FaceValue(2)", response),
- description: parts[3],
- voucherType: (Batch.Vouchertype)ParseInt(parts[6], "Product.VoucherType(6)", response),
- discountPercentage: ParseDecimal(parts[7], "Product.DiscountPercentage(7)", response)
- );
- }
- return catalogue;
- }
- private bool ParseBool(string value, string valueName, string fullResponse)
- {
- bool ret;
- if (! bool.TryParse(value, out ret))
- {
- ThrowParseError(value, valueName, "bool", fullResponse);
- }
- return ret;
- }
- private DateTime ParseDateTime(string value, string valueName, string fullResponse)
- {
- DateTime ret;
- if (!DateTime.TryParse(value, out ret))
- {
- ThrowParseError(value, valueName, "DateTime", fullResponse);
- }
- return ret;
- }
- private decimal ParseDecimal(string value, string valueName, string fullResponse)
- {
- decimal ret;
- if (!decimal.TryParse(value, out ret))
- {
- double fallback;
- if (!double.TryParse(value, out fallback))
- {
- ThrowParseError(value, valueName, "decimal", fullResponse);
- }
- return (decimal)fallback;
- }
- return ret;
- }
- private int ParseInt(string value, string valueName, string fullResponse)
- {
- int ret;
- if (!int.TryParse(value, out ret))
- {
- ThrowParseError(value, valueName, "int", fullResponse);
- }
- return ret;
- }
- public async Task<OrderResponse> PlaceOrderAsync(int accountId, Product product, int quantity,
- string customerReference, string internalReference, Guid? orderGuid, byte[] key)
- {
- if (key.Length != 24)
- {
- throw new ArgumentException("24 byte key expected", nameof(key));
- }
- _logger.LogDebug(
- "Placing order for {0}: date={1} quantity={2} productId={3} productDescription={4} networkId={5} networkName={6} customerRef={7} internalRef={8}",
- LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber),
- DateTimeOffset.UtcNow,
- quantity,
- product.Id,
- product.Description,
- product.Network.Id,
- product.Network.Name,
- customerReference,
- internalReference
- );
- await WriteMessageAsync(new MessageBuilder()
- .Append("Order ")
- .Append(Encrypt(new StringBuilder()
- .Append(product.Id)
- .Append("|")
- .Append(quantity)
- .Append("|")
- .Append(customerReference)
- .Append("|2|") // EncType: 0:None, 1:DES, 2:Triple DES
- .Append(BitConverter.ToString(key, 0, 8).Replace("-", ""))
- .Append("|")
- .Append(BitConverter.ToString(key, 8, 8).Replace("-", ""))
- .Append("|")
- .Append(BitConverter.ToString(key, 16, 8).Replace("-", ""))
- .Append("|")
- .Append(internalReference)
- .ToString()))).ConfigureAwait(false);
- var response = ExpectResponse(Decrypt(await ReadMessageAsync().ConfigureAwait(false)), "OK");
- _logger.LogDebug("Order response for {0} customerRef={1} internalRef={2}: {3}",
- LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber),
- customerReference,
- internalReference,
- response
- );
- var parts = response.Split('|');
- return new OrderResponse()
- {
- Batch = new Batch()
- {
- Id = ParseInt(parts[1], "Batch.Id(1)", response),
- OrderReference = parts[2],
- RequestedQuantity = ParseInt(parts[3], "Batch.RequestQuantity(3)", response),
- DeliveredQuantity = ParseInt(parts[4], "Batch.DeliveredQuantity(4)", response),
- Cost = ParseDecimal(parts[5], "Batch.Cost(5)", response),
- InternalReference = internalReference,
- OrderGuid = orderGuid,
- AccountId = accountId,
- VendorId = _vendorId,
- ProductId = product.Id,
- ProductDescription = product.Description,
- VoucherType = product.VoucherType,
- FaceValue = product.FaceValue,
- DiscountPercentage = product.DiscountPercentage,
- NetworkId = product.Network.Id,
- NetworkName = product.Network.Name,
- OrderDate = DateTimeOffset.UtcNow,
- OrderedById = _userId,
- ReadyForDownload = false
- },
- RemainingBalance = ParseDecimal(parts[6], "Batch.RemainingBalance(6)", response)
- };
- }
- private async Task<byte[]> ReadBytesAsync(int count)
- {
- int totalBytesRead = 0;
- byte[] buffer = new byte[count];
- while (totalBytesRead < count)
- {
- int bytesRead = await _connectionStream.ReadAsync(buffer, totalBytesRead, count - totalBytesRead).ConfigureAwait(false);
- if (bytesRead == 0)
- throw new Exception("Connection closed unexpectedly");
- totalBytesRead += bytesRead;
- }
- return buffer;
- }
- private async Task<string> ReadMessageAsync()
- {
- byte[] buffer = await ReadBytesAsync(2).ConfigureAwait(false);
- int size = buffer[0] * 256 + buffer[1];
- if (size <= 0)
- {
- throw new Exception("Invalid message size");
- }
- var response = Encoding.ASCII.GetString(await ReadBytesAsync(size).ConfigureAwait(false));
- if (_logResponses)
- {
- _logger.LogDebug("Response for {0}: {1}", LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber), response);
- }
- return response;
- }
- public int ReceiveTimeout { get; set; }
- public async Task ReExportBatchAsync(int batchId, byte[] key)
- {
- if (key.Length != 24)
- {
- throw new ArgumentException("24 byte key expected", nameof(key));
- }
- await WriteMessageAsync(new MessageBuilder()
- .Append("ReExport ")
- .Append(Encrypt(new StringBuilder()
- .Append(batchId)
- .Append("|2|") // EncType: 0:None, 1:DES, 2:Triple DES
- .Append(BitConverter.ToString(key, 0, 8).Replace("-", ""))
- .Append("|")
- .Append(BitConverter.ToString(key, 8, 8).Replace("-", ""))
- .Append("|")
- .Append(BitConverter.ToString(key, 16, 8).Replace("-", ""))
- .ToString()))).ConfigureAwait(false);
- ExpectResponse(Decrypt(await ReadMessageAsync().ConfigureAwait(false)), "OK");
- }
- public int SendTimeout { get; set; }
- private void ThrowParseError(string value, string valueName, string valueType, string fullResponse)
- {
- _logger.LogError(
- "Failed to parse value: valueType={0} valueName={1} value={2} fullResponse={3} {4}",
- valueType,
- valueName,
- value,
- fullResponse,
- LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber)
- );
- throw new Exception(String.Format("Invalid value for {0}", valueName));
- }
- private async Task WriteMessageAsync(MessageBuilder message)
- {
- byte[] data = message.GetBytes();
- if (_logResponses)
- {
- _logger.LogDebug("Request for {0}: {1}",
- LoginCredentials.Format(_userId, _username, _vendorId, _serialNumber),
- Encoding.ASCII.GetString(data, 2, data.Length - 2)
- );
- }
- await _connectionStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
- }
- }
- }
|