Web API for the bulk printing desktop application.

Utils.cs 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. using BulkPrintingAPI.Configuration;
  2. using MAX.Models;
  3. using Microsoft.AspNetCore.Http;
  4. using Microsoft.EntityFrameworkCore;
  5. using Microsoft.Extensions.Caching.Memory;
  6. using Microsoft.Extensions.Logging;
  7. using System;
  8. using System.Diagnostics;
  9. using System.IO;
  10. using System.Security.Claims;
  11. using System.Security.Cryptography;
  12. using System.Text;
  13. using System.Threading.Tasks;
  14. namespace BulkPrintingAPI.Controllers
  15. {
  16. public static class Utils
  17. {
  18. public static byte GetMinimalBitmask(byte value)
  19. {
  20. if (value == 0)
  21. {
  22. return 0;
  23. }
  24. value--;
  25. value |= (byte)(value >> 1);
  26. value |= (byte)(value >> 2);
  27. value |= (byte)(value >> 4);
  28. return value;
  29. }
  30. public static string GenerateRandomStringFromAlphabet(int length, string alphabet =
  31. "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+=[{]};:<>|./?")
  32. {
  33. if (alphabet.Length > 255)
  34. {
  35. throw new ArgumentException("Alphabet too long", nameof(alphabet));
  36. }
  37. var minimalBitmask = GetMinimalBitmask((byte)alphabet.Length);
  38. var builder = new StringBuilder(length);
  39. var randomBytes = new byte[length * 2];
  40. int offset = randomBytes.Length;
  41. using (var rng = RandomNumberGenerator.Create())
  42. {
  43. while (builder.Length < length)
  44. {
  45. if (offset >= randomBytes.Length)
  46. {
  47. rng.GetBytes(randomBytes);
  48. offset = 0;
  49. }
  50. // We constrain c using minimalBitmask (minimal power of
  51. // two greater than alphabetSize, minus 1) so that it is as
  52. // close to fitting within alphabet.Length as possible while still
  53. // being evenly distributed. We then throw away any numbers >=
  54. // alphabet.Length. This is wasteful but it is the only way of
  55. // generating an unbiased selection, and random number
  56. // generators are very fast.
  57. byte c = (byte)(randomBytes[offset] & minimalBitmask);
  58. if (c < alphabet.Length)
  59. {
  60. builder.Append(alphabet[c]);
  61. }
  62. offset++;
  63. }
  64. }
  65. return builder.ToString();
  66. }
  67. public static byte[] GenerateAndAesEncryptKey(int keyLength, byte[] encryptingKey)
  68. {
  69. var key = new byte[keyLength];
  70. using (var rng = RandomNumberGenerator.Create())
  71. {
  72. rng.GetBytes(key);
  73. }
  74. using (var aes = Aes.Create())
  75. using (var encryptor = aes.CreateEncryptor(encryptingKey, new byte[16]))
  76. {
  77. return MAX.Utils.Transform(encryptor, key);
  78. }
  79. }
  80. public static byte[] GenerateAndAesEncryptPassword(int passwordLength, byte[] encryptingKey)
  81. {
  82. var password = GenerateRandomStringFromAlphabet(passwordLength);
  83. using (var aes = Aes.Create())
  84. using (var encryptor = aes.CreateEncryptor(encryptingKey, new byte[16]))
  85. {
  86. return MAX.Utils.Transform(encryptor, Encoding.ASCII.GetBytes(password));
  87. }
  88. }
  89. public static byte[] AesDecryptBytes(byte[] cipherText, byte[] key)
  90. {
  91. using (var aes = Aes.Create())
  92. using (var decryptor = aes.CreateDecryptor(key, new byte[16]))
  93. {
  94. return MAX.Utils.Transform(decryptor, cipherText);
  95. }
  96. }
  97. public static string AesDecryptString(byte[] cipherText, byte[] key)
  98. {
  99. return Encoding.ASCII.GetString(AesDecryptBytes(cipherText, key));
  100. }
  101. public static byte[] AesReEncrypt(byte[] cipherText, byte[] originalKey, byte[] newKey)
  102. {
  103. using (var aes = Aes.Create())
  104. {
  105. byte[] plainText;
  106. using (var decryptor = aes.CreateDecryptor(originalKey, new byte[16]))
  107. {
  108. plainText = MAX.Utils.Transform(decryptor, cipherText);
  109. }
  110. using (var encryptor = aes.CreateEncryptor(newKey, new byte[16]))
  111. {
  112. return MAX.Utils.Transform(encryptor, plainText);
  113. }
  114. }
  115. }
  116. public static async Task<MAX.LoginCredentials> SyncUserAndVendorWithDbAsync(MAXContext context,
  117. DataEncryptionOptions dataEncryptionOptions, User user, string password, int vendorId,
  118. string serialNumber)
  119. {
  120. var warehouse = user.Account.Warehouse;
  121. var existingWarehouse = await context.Warehouses.FindAsync(warehouse.Id).ConfigureAwait(false);
  122. if (existingWarehouse == null)
  123. {
  124. context.Warehouses.Add(warehouse);
  125. }
  126. else
  127. {
  128. existingWarehouse.Name = warehouse.Name;
  129. warehouse = existingWarehouse;
  130. }
  131. var account = user.Account;
  132. var existingAccount = await context.Accounts.FindAsync(account.Id).ConfigureAwait(false);
  133. if (existingAccount == null)
  134. {
  135. context.Accounts.Add(account);
  136. }
  137. else
  138. {
  139. existingAccount.Name = account.Name;
  140. existingAccount.Status = account.Status;
  141. existingAccount.Reference = account.Reference;
  142. existingAccount.Balance = account.Balance;
  143. account = existingAccount;
  144. }
  145. account.Warehouse = warehouse;
  146. var existingUser = await context.Users.FindAsync(user.Id).ConfigureAwait(false);
  147. if (existingUser == null)
  148. {
  149. context.Users.Add(user);
  150. }
  151. else
  152. {
  153. existingUser.Username = user.Username;
  154. existingUser.FirstName = user.FirstName;
  155. existingUser.Surname = user.Surname;
  156. existingUser.Enabled = user.Enabled;
  157. existingUser.Level = user.Level;
  158. existingUser.System = user.System;
  159. existingUser.LastLogin = user.LastLogin;
  160. existingUser.CanPrintOffline = user.CanPrintOffline;
  161. existingUser.CanPrintOnline = user.CanPrintOnline;
  162. existingUser.CanReprintOffline = user.CanReprintOffline;
  163. existingUser.CanReprintOnline = user.CanReprintOnline;
  164. existingUser.OfflinePrintValue = user.OfflinePrintValue;
  165. existingUser.OfflineReprintValue = user.OfflineReprintValue;
  166. existingUser.OnlinePrintValue = user.OnlinePrintValue;
  167. existingUser.OnlineReprintValue = user.OnlineReprintValue;
  168. existingUser.BulkExport = user.BulkExport;
  169. existingUser.BulkExportMaxValue = user.BulkExportMaxValue;
  170. existingUser.BulkOrder = user.BulkOrder;
  171. existingUser.BulkOrderMaxValue = user.BulkOrderMaxValue;
  172. existingUser.BulkViewPins = user.BulkViewPins;
  173. existingUser.BulkReExport = user.BulkReExport;
  174. user = existingUser;
  175. }
  176. user.Account = account;
  177. var vendor = await context.Vendors.FindAsync(vendorId).ConfigureAwait(false);
  178. if (vendor == null)
  179. {
  180. vendor = new Vendor()
  181. {
  182. Id = vendorId,
  183. SerialNumber = serialNumber,
  184. Account = account,
  185. EncryptedDatabasePassword =
  186. // A password length of 31 bytes results keeps us inside 2 AES blocks (32 bytes)
  187. // as padding is a minimum of 1 byte.
  188. GenerateAndAesEncryptPassword(31, dataEncryptionOptions.DefaultKey),
  189. EncryptedVoucherKey =
  190. GenerateAndAesEncryptKey(24, dataEncryptionOptions.DefaultKey)
  191. };
  192. context.Add(vendor);
  193. }
  194. else
  195. {
  196. vendor.SerialNumber = serialNumber;
  197. vendor.Account = account;
  198. }
  199. await context.SaveChangesAsync().ConfigureAwait(false);
  200. return new MAX.LoginCredentials
  201. {
  202. User = user,
  203. Vendor = vendor,
  204. Password = password
  205. };
  206. }
  207. public static async Task<MAX.LoginCredentials> GetLoginCredentialsFromRequestAsync(
  208. HttpContext httpContext, MAXContext dbContext)
  209. {
  210. var authInfo = await httpContext.Authentication.GetAuthenticateInfoAsync("Bearer");
  211. if (authInfo == null)
  212. {
  213. throw new Exception("Not authenticated");
  214. }
  215. int userId = -1;
  216. int vendorId = -1;
  217. string password = null;
  218. foreach (var claim in authInfo.Principal.Claims)
  219. {
  220. switch (claim.Type)
  221. {
  222. case ClaimTypes.NameIdentifier:
  223. userId = int.Parse(claim.Value);
  224. break;
  225. case "xvid":
  226. vendorId = int.Parse(claim.Value);
  227. break;
  228. case "xpwd":
  229. password = claim.Value;
  230. break;
  231. }
  232. }
  233. var credentials = new MAX.LoginCredentials()
  234. {
  235. User = await dbContext.Users
  236. .Include(u => u.Account)
  237. .ThenInclude(a => a.Warehouse)
  238. .SingleOrDefaultAsync(u => u.Id == userId)
  239. .ConfigureAwait(false),
  240. Vendor = await dbContext.Vendors
  241. .Include(v => v.Account) // Technically not necessary as the account is loaded above
  242. .SingleOrDefaultAsync(v => v.Id == vendorId)
  243. .ConfigureAwait(false),
  244. Password = password
  245. };
  246. if ((credentials.User == null) || (credentials.Vendor == null))
  247. {
  248. throw new Exception(String.Format(
  249. "Missing user or vendor information for userId={0} vendorId={1}",
  250. userId, vendorId));
  251. }
  252. return credentials;
  253. }
  254. public static async Task<ProductCatalogue> GetProductCatalogueAsync(
  255. MAX.ClientFactory clientFactory, ILogger logger, IMemoryCache cache,
  256. MAX.LoginCredentials credentials, bool forceRefresh)
  257. {
  258. var key = CacheKeys.GetAccountProductsKey(credentials.User.AccountId);
  259. if (forceRefresh)
  260. {
  261. cache.Remove(key);
  262. }
  263. return await cache.GetOrCreateAsync(key, entry =>
  264. {
  265. // TODO: set expiry time, etc.
  266. return MAX.Utils.GetProductCatalogueAsync(clientFactory, logger, credentials);
  267. }).ConfigureAwait(false);
  268. }
  269. [Conditional("DEBUG")]
  270. private static void VoucherSanityCheck(Batch batch, decimal faceValue, int batchId,
  271. string productDescription)
  272. {
  273. if (batch.FaceValue != faceValue)
  274. {
  275. throw new Exception("Voucher face value mismatch");
  276. }
  277. if (batch.Id != batchId)
  278. {
  279. throw new Exception("Voucher batch ID mismatch");
  280. }
  281. if (batch.ProductDescription != productDescription)
  282. {
  283. throw new Exception("Voucher product description mismatch");
  284. }
  285. }
  286. public static async Task DownloadVouchersAsync(SFTPOptions sftpOptions, MAXContext context,
  287. ILogger logger, Batch batch)
  288. {
  289. var remoteFileName = string.Format("{0}_{1}.dat", batch.Account.Id, batch.Id);
  290. using (var voucherStream = new MemoryStream())
  291. {
  292. var connectionInfo = new Renci.SshNet.ConnectionInfo(
  293. sftpOptions.Host,
  294. sftpOptions.Port,
  295. sftpOptions.Username,
  296. new Renci.SshNet.PasswordAuthenticationMethod(sftpOptions.Username, sftpOptions.Password)
  297. );
  298. connectionInfo.Timeout = TimeSpan.FromSeconds(sftpOptions.ConnectTimeout);
  299. await Task.Run(() =>
  300. {
  301. using (var sshClient = new Renci.SshNet.SftpClient(connectionInfo))
  302. {
  303. sshClient.Connect();
  304. sshClient.DownloadFile(remoteFileName, voucherStream);
  305. }
  306. });
  307. voucherStream.Position = 0;
  308. using (var streamReader = new StreamReader(voucherStream))
  309. {
  310. while (streamReader.Peek() >= 0)
  311. {
  312. var line = streamReader.ReadLine();
  313. var parts = line.Split('|');
  314. VoucherSanityCheck(batch, decimal.Parse(parts[4]), int.Parse(parts[5]),
  315. parts[7]);
  316. context.Add(new Voucher()
  317. {
  318. Id = int.Parse(parts[0]),
  319. ExpiryDate = DateTime.Parse(parts[1]),
  320. Serial = parts[2],
  321. EncryptedPIN = parts[3],
  322. SequenceNumber = int.Parse(parts[6]),
  323. Batch = batch
  324. });
  325. }
  326. }
  327. batch.ReadyForDownload = true;
  328. await context.SaveChangesAsync().ConfigureAwait(false);
  329. try
  330. {
  331. await Task.Run(() =>
  332. {
  333. using (var sshClient = new Renci.SshNet.SftpClient(connectionInfo))
  334. {
  335. sshClient.Connect();
  336. sshClient.DeleteFile(remoteFileName);
  337. }
  338. });
  339. }
  340. catch (Exception e)
  341. {
  342. logger.LogWarning("Failed to delete file on FTP server: {0}: {1}", remoteFileName, e.Message);
  343. }
  344. }
  345. }
  346. }
  347. }