Web API for the bulk printing desktop application.

Utils.cs 13KB

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