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 TcpClient _connection = null; private NetworkStream _connectionStream = null; private TripleDES _des = null; private bool _disposed = false; public Client(ILogger logger, 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; ConnectTimeout = 10000; ReceiveTimeout = 10000; SendTimeout = 10000; } public Client(ILogger logger, string host, int port, LoginCredentials credentials) : this(logger, host, port, credentials.Vendor.Id, credentials.Vendor.SerialNumber, credentials.User.Id, credentials.User.Username, credentials.Password) { } public void Close() { Dispose(true); } public async Task 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 = await ReadMessageAsync().ConfigureAwait(false); if (!response.StartsWith("Hi ")) { _logger.LogError("Device authentication failed: {0}", response); return null; } // Request server RSA key await WriteMessageAsync(new MessageBuilder().Append("PK")).ConfigureAwait(false); response = await ReadMessageAsync().ConfigureAwait(false); // Key exchange _des = TripleDES.Create(); _des.IV = new byte[8]; await WriteMessageAsync(new MessageBuilder() .Append("3D ") .Append(EncryptRSA(response, BitConverter.ToString(_des.Key).Replace("-", "")))).ConfigureAwait(false); response = await ReadMessageAsync().ConfigureAwait(false); if (!response.StartsWith("OK")) { throw new Exception(String.Format("Key exchange failed: {0}", response)); } // User authentication await WriteMessageAsync(new MessageBuilder() .Append("User ") .Append(Encrypt(new StringBuilder() .Append(_userId) .Append("|") .Append(_username) .Append("|") .Append(_password).ToString()))).ConfigureAwait(false); response = Decrypt(await ReadMessageAsync().ConfigureAwait(false)); if (response.StartsWith("OK")) { var parts = response.Split('|'); var user = new User() { Id = _userId, Username = _username, FirstName = parts[4], Surname = parts[3], Enabled = bool.Parse(parts[6]), Level = (User.UserLevel)int.Parse(parts[1]), System = int.Parse(parts[2]), LastLogin = DateTime.Parse(parts[5]) }; if (user.Level == User.UserLevel.CustomUser) { user.CanPrintOffline = bool.Parse(parts[7]); user.OfflinePrintValue = decimal.Parse(parts[8]); user.CanPrintOnline = bool.Parse(parts[9]); user.OnlinePrintValue = decimal.Parse(parts[10]); user.CanReprintOffline = bool.Parse(parts[11]); user.OfflineReprintValue = decimal.Parse(parts[12]); user.CanReprintOnline = bool.Parse(parts[13]); user.OnlineReprintValue = decimal.Parse(parts[14]); } return user; } else if (response.StartsWith("ER")) { _logger.LogInformation("User authentication failed: {0}", response); return null; } else { throw new Exception(String.Format("Invalid user information response: {0}", response)); } } 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) { return Utils.TripleDESDecrypt(cipherText, _des); } 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(); } } public async Task GetAccountAsync() { await WriteMessageAsync(new MessageBuilder().Append("Acc")).ConfigureAwait(false); var response = Decrypt(await ReadMessageAsync().ConfigureAwait(false)); if (response.StartsWith("OK")) { var parts = response.Split('|'); return new Account() { Id = int.Parse(parts[1]), Name = parts[2], Balance = decimal.Parse(parts[3]), Status = (Account.AccountStatus)int.Parse(parts[4]), Reference = parts[5], Warehouse = new Warehouse() { Id = int.Parse(parts[6]), Name = parts[7] } }; } else { throw new Exception(String.Format("Invalid account information response: {0}", response)); } } public async Task GetProductCatalogueAsync(Account account) { var encryptedWarehouseName = Encrypt(account.Warehouse.Name); await WriteMessageAsync(new MessageBuilder() .Append("Pdt ") .Append(encryptedWarehouseName)).ConfigureAwait(false); var response = Decrypt(await ReadMessageAsync().ConfigureAwait(false)); if (response.StartsWith("OK")) { var parts = response.Split('|'); var count = int.Parse(parts[1]); 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 = Decrypt(await ReadMessageAsync().ConfigureAwait(false)); if (response.StartsWith("OK")) { parts = response.Split('|'); int networkId = int.Parse(parts[4]); Network network; if (! catalogue.NetworkMap.TryGetValue(networkId, out network)) { network = catalogue.AddNetwork(networkId, parts[5]); } catalogue.AddProduct( network: network, id: int.Parse(parts[1]), faceValue: decimal.Parse(parts[2]), description: parts[3], voucherType: (Batch.Vouchertype)int.Parse(parts[6]), discountPercentage: decimal.Parse(parts[7]) ); } else { throw new Exception(String.Format("Invalid product item response: {0}", response)); } } return catalogue; } else { throw new Exception(String.Format("Invalid product catalogue response: {0}", response)); } } public async Task PlaceOrderAsync(int accountId, Product product, int quantity, string customerReference, Guid? orderGuid, byte[] key) { if (key.Length != 24) { throw new ArgumentException("24 byte key expected", nameof(key)); } 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("-", "")) .ToString()))).ConfigureAwait(false); var response = Decrypt(await ReadMessageAsync().ConfigureAwait(false)); if (response.StartsWith("OK")) { var parts = response.Split('|'); return new OrderResponse() { Batch = new Batch() { Id = int.Parse(parts[1]), OrderReference = parts[2], RequestedQuantity = int.Parse(parts[3]), DeliveredQuantity = int.Parse(parts[4]), Cost = decimal.Parse(parts[5]), 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 = decimal.Parse(parts[6]) }; } else { throw new Exception(string.Format("Invalid order response: {0}", response)); } } private async Task 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 ReadMessageAsync() { byte[] buffer = await ReadBytesAsync(2).ConfigureAwait(false); int size = buffer[0] * 256 + buffer[1]; if (size <= 0) { throw new Exception("Invalid message size"); } return Encoding.ASCII.GetString(await ReadBytesAsync(size).ConfigureAwait(false)); } public int ReceiveTimeout { get; set; } public int SendTimeout { get; set; } private async Task WriteMessageAsync(MessageBuilder message) { byte[] data = message.GetBytes(); await _connectionStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); } } }