RestService.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. using System.Reflection;
  2. using InABox.Clients;
  3. using InABox.Core;
  4. using InABox.Database;
  5. using InABox.IPC;
  6. namespace InABox.API
  7. {
  8. public class RestService
  9. {
  10. public static bool CheckPasswordExpiration { get; set; } = true;
  11. public static bool IsHTTPS { get; set; } = false;
  12. protected static void GarbageCollection()
  13. {
  14. //DateTime now = DateTime.Now;
  15. //GC.Collect();
  16. //GC.WaitForPendingFinalizers();
  17. //GC.Collect();
  18. //Logger.Send(LogType.Information, "", String.Format("Garbage Collection takes {0:F2}ms", (DateTime.Now - now).TotalMilliseconds));
  19. }
  20. protected static Guid ValidateRequest(Request request, out string? userID)
  21. {
  22. var session = request.Credentials.Session;
  23. if (session != Guid.Empty)
  24. {
  25. var valid = CredentialsCache.Validate(session, out userID);
  26. if (valid != Guid.Empty)
  27. {
  28. CredentialsCache.RefreshSessionExpiry(session);
  29. }
  30. return valid;
  31. }
  32. else if (request.Credentials.UserID is not null
  33. && request.Credentials.Password is not null
  34. && CredentialsCache.IsBypassed(request.Credentials.UserID, request.Credentials.Password))
  35. {
  36. userID = "";
  37. return CoreUtils.FullGuid;
  38. }
  39. userID = null;
  40. return Guid.Empty;
  41. }
  42. public static MultiQueryResponse QueryMultiple(MultiQueryRequest request, bool isSecure, Logger logger)
  43. {
  44. var start = DateTime.Now;
  45. var response = new MultiQueryResponse();
  46. var userguid = ValidateRequest(request, out var userid);
  47. if (userguid == Guid.Empty)
  48. return response;
  49. logger.Send(LogType.Information, userid, string.Format("[{0} {1}] QueryMultiple({2})",
  50. request.Credentials.Platform,
  51. request.Credentials.Version,
  52. request.Queries.Count));
  53. try
  54. {
  55. var queries = new Dictionary<string, IQueryDef>();
  56. foreach (var (key, query) in request.Queries)
  57. {
  58. var type = CoreUtils.GetEntity(query.Type);
  59. if (type.IsAssignableTo(typeof(ISecure)) && !isSecure)
  60. {
  61. logger.Send(LogType.Error, userid, $"{query.Type} is a secure entity. Request failed");
  62. }
  63. else
  64. {
  65. queries.Add(key, Activator.CreateInstance(typeof(QueryDef<>).MakeGenericType(type), query.Filter, query.Columns, query.Sort) as IQueryDef);
  66. }
  67. }
  68. response.Tables = DbFactory.QueryMultiple(queries, userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
  69. response.Status = StatusCode.OK;
  70. logger.Send(LogType.Information, userid,
  71. string.Format("[{0} {1}] [{2:D8}] QueryMultiple Complete", request.Credentials.Platform, request.Credentials.Version,
  72. (int)DateTime.Now.Subtract(start).TotalMilliseconds));
  73. }
  74. catch (Exception err)
  75. {
  76. response.Status = StatusCode.Error;
  77. response.Messages.Add(err.Message);
  78. logger.Send(LogType.Error, userid,
  79. string.Format("[{0} {1}] [{2:D8}] QueryMultiple Failed: {3} ", request.Credentials.Platform, request.Credentials.Version,
  80. (int)DateTime.Now.Subtract(start).TotalMilliseconds, err.Message));
  81. }
  82. GarbageCollection();
  83. return response;
  84. }
  85. public static ValidateResponse Validate(ValidateRequest request, Logger logger)
  86. {
  87. var response = new ValidateResponse();
  88. User? user = null;
  89. bool reLogin = false;
  90. if (request.Credentials.Session != Guid.Empty)
  91. {
  92. var session = request.Credentials.Session;
  93. user = CredentialsCache.Validate(session);
  94. if (user != null)
  95. {
  96. logger.Send(LogType.Information, "", $"{session} re-logged in!");
  97. CredentialsCache.RefreshSessionExpiry(session);
  98. reLogin = true;
  99. }
  100. }
  101. if (user == null)
  102. {
  103. if (request.UsePIN)
  104. {
  105. logger.Send(LogType.Information, "", $"Login request with PIN {request.PIN}");
  106. user = CredentialsCache.ValidateUser(request.PIN);
  107. }
  108. else
  109. {
  110. var userID = request.UserID ?? request.Credentials.UserID;
  111. var password = request.Password ?? request.Credentials.Password;
  112. if (userID == null || password == null)
  113. return response.Status(StatusCode.Error);
  114. user = CredentialsCache.ValidateUser(userID, password);
  115. if (user?.ID != CoreUtils.FullGuid)
  116. {
  117. logger.Send(LogType.Information, userID, $"Login request for {userID}");
  118. }
  119. }
  120. }
  121. response.Status = StatusCode.OK;
  122. if (user == null)
  123. {
  124. logger.Send(LogType.Information, "", $"Login failed!");
  125. response.ValidationStatus = ValidationStatus.INVALID;
  126. }
  127. else if (CheckPasswordExpiration && !request.UsePIN && user.PasswordExpiration > DateTime.MinValue && user.PasswordExpiration < DateTime.Now)
  128. {
  129. logger.Send(LogType.Information, user.UserID, $"Password for ({user.UserID}) has expired!");
  130. response.ValidationStatus = ValidationStatus.PASSWORD_EXPIRED;
  131. }
  132. else if (reLogin)
  133. {
  134. logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) success!");
  135. response.ValidationStatus = ValidationStatus.VALID;
  136. response.UserGuid = user.ID;
  137. response.UserID = user.UserID;
  138. response.SecurityID = user.SecurityGroup.ID;
  139. response.Session = request.Credentials.Session;
  140. response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
  141. }
  142. else if (user.ID == CoreUtils.FullGuid || !user.Use2FA)
  143. {
  144. logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) success!");
  145. response.ValidationStatus = ValidationStatus.VALID;
  146. response.UserGuid = user.ID;
  147. response.UserID = user.UserID;
  148. response.SecurityID = user.SecurityGroup.ID;
  149. response.Session = user.ID == CoreUtils.FullGuid ?
  150. CredentialsCache.NewSession(user, true, DateTime.MaxValue) :
  151. CredentialsCache.NewSession(user, true);
  152. response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
  153. }
  154. else
  155. {
  156. logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) requires 2FA. Sending code...");
  157. var session = CredentialsCache.SendCode(user.ID, out var recipient);
  158. if (session != null)
  159. {
  160. response.ValidationStatus = ValidationStatus.REQUIRE_2FA;
  161. response.UserGuid = user.ID;
  162. response.UserID = user.UserID;
  163. response.SecurityID = user.SecurityGroup.ID;
  164. response.Session = session ?? Guid.Empty;
  165. response.Recipient2FA = recipient;
  166. response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
  167. }
  168. else
  169. {
  170. response.Status = StatusCode.Error;
  171. response.Messages.Add("Code failed to send");
  172. }
  173. }
  174. return response;
  175. }
  176. public static Check2FAResponse Check2FA(Check2FARequest request, Logger logger)
  177. {
  178. var response = new Check2FAResponse();
  179. logger.Send(LogType.Information, "", $"2FA check for session {request.Credentials.Session} and code {request.Code}");
  180. response.Valid = CredentialsCache.ValidateCode(request.Credentials.Session, request.Code);
  181. response.Status = StatusCode.OK;
  182. return response;
  183. }
  184. public static InfoResponse Info(InfoRequest request, Logger logger)
  185. {
  186. var response = new InfoResponse(new DatabaseInfo(DbFactory.ColorScheme, DbFactory.Logo, CoreUtils.GetVersion(), IsHTTPS, DbFactory.RestPort, DbFactory.RPCPort, DbFactory.ID));
  187. response.Status = StatusCode.OK;
  188. return response;
  189. }
  190. }
  191. public class RestService<TEntity> : RestService where TEntity : Entity, new()
  192. {
  193. private static string SimpleName(Type t)
  194. {
  195. return t.Name.Split('.').Last();
  196. }
  197. public static QueryResponse<TEntity> List(QueryRequest<TEntity> request, Logger logger)
  198. {
  199. var start = DateTime.Now;
  200. var response = new QueryResponse<TEntity>();
  201. var userguid = ValidateRequest(request, out var userid);
  202. if (userguid == Guid.Empty)
  203. return response.Status(StatusCode.Unauthenticated);
  204. logger.Send(LogType.Information, userid,
  205. string.Format("[{0} {1}] Query{2}: Filter=[{3}] Sort=[{4}]", request.Credentials.Platform, request.Credentials.Version,
  206. SimpleName(typeof(TEntity)), request.Filter != null ? request.Filter.ToString() : "",
  207. request.Sort != null ? request.Sort.AsOData() : ""));
  208. try
  209. {
  210. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
  211. response.Items = store.Query(request.Filter, request.Columns, request.Sort, request.Range);
  212. response.Status = StatusCode.OK;
  213. logger.Send(LogType.Information, userid,
  214. string.Format("[{0} {1}] [{2:D8}] Query{3} Complete: {4} records / {5} columns returned", request.Credentials.Platform,
  215. request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)),
  216. response.Items.Rows.Count, response.Items.Columns.Count));
  217. }
  218. catch (Exception err)
  219. {
  220. response.Status = StatusCode.Error;
  221. response.Messages.Add(err.Message);
  222. logger.Send(LogType.Error, userid,
  223. string.Format("[{0} {1}] [{2:D8}] Query{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
  224. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
  225. }
  226. GarbageCollection();
  227. return response;
  228. }
  229. /*public static LoadResponse<TEntity> Load(LoadRequest<TEntity> request)
  230. {
  231. var start = DateTime.Now;
  232. var response = new LoadResponse<TEntity>();
  233. var userguid = ValidateRequest(request, out var userid);
  234. if (userguid == Guid.Empty)
  235. return response.Status(StatusCode.Unauthenticated);
  236. Logger.Send(LogType.Information, userid,
  237. string.Format("[{0} {1}] Load{2}: Filter=[{3}]", request.Credentials.Platform, request.Credentials.Version,
  238. SimpleName(typeof(TEntity)),
  239. request.Filter != null ? request.Filter.AsOData() : ""));
  240. try
  241. {
  242. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
  243. response.Items = store.Load(request.Filter, request.Sort);
  244. response.Status = StatusCode.OK;
  245. Logger.Send(LogType.Information, userid,
  246. string.Format("[{0} {1}] [{2:D8}] Load{3} Complete: {4} records returned", request.Credentials.Platform,
  247. request.Credentials.Version,
  248. (uint)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), response.Items.Count()));
  249. }
  250. catch (Exception err)
  251. {
  252. response.Status = StatusCode.Error;
  253. response.Messages.Add(err.Message);
  254. Logger.Send(LogType.Error, userid,
  255. string.Format("[{0} {1}] [{2:D8}] Load{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
  256. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
  257. }
  258. GarbageCollection();
  259. return response;
  260. }*/
  261. public static SaveResponse<TEntity> Save(SaveRequest<TEntity> request, Logger logger)
  262. {
  263. var start = DateTime.Now;
  264. var response = new SaveResponse<TEntity>();
  265. var userguid = ValidateRequest(request, out var userid);
  266. if (userguid == Guid.Empty)
  267. return response.Status(StatusCode.Unauthenticated);
  268. logger.Send(LogType.Information, userid,
  269. string.Format("[{0} {1}] Save{2}: Data=[{3}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)),
  270. request.Item != null ? request.Item.ToString() : request + " (null)"));
  271. try
  272. {
  273. var e = request.Item;
  274. var preSaveChanges = BaseObjectExtensions.GetOriginaValues(e);
  275. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
  276. store.Save(e, request.AuditNote);
  277. if (request.ReturnOnlyChanged)
  278. {
  279. var currentChanges = BaseObjectExtensions.GetOriginaValues(e);
  280. foreach (var (key, value) in currentChanges)
  281. {
  282. if (preSaveChanges.TryGetValue(key, out var oldValue))
  283. {
  284. if (!Equals(value, oldValue))
  285. {
  286. response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key));
  287. }
  288. }
  289. else
  290. {
  291. response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key));
  292. }
  293. }
  294. }
  295. else
  296. {
  297. response.Item = e;
  298. }
  299. response.Status = StatusCode.OK;
  300. logger.Send(LogType.Information, userid,
  301. string.Format("[{0} {1}] [{2:D8}] Save{3} Data=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
  302. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), request.Item));
  303. CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
  304. }
  305. catch (Exception err)
  306. {
  307. response.Status = StatusCode.Error;
  308. response.Messages.Add(err.Message);
  309. logger.Send(LogType.Error, userid,
  310. string.Format("[{0} {1}] [{2:D8}] Save{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
  311. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
  312. }
  313. GarbageCollection();
  314. return response;
  315. }
  316. public static MultiSaveResponse<TEntity> MultiSave(MultiSaveRequest<TEntity> request, Logger logger)
  317. {
  318. var start = DateTime.Now;
  319. var response = new MultiSaveResponse<TEntity>();
  320. var userguid = ValidateRequest(request, out var userid);
  321. if (userguid == Guid.Empty)
  322. return response.Status(StatusCode.Unauthenticated);
  323. logger.Send(LogType.Information, userid,
  324. string.Format("[{0} {1}] MultiSave{2}({3}) Data=[{4}]", request.Credentials.Platform, request.Credentials.Version,
  325. SimpleName(typeof(TEntity)), request.Items.Length,
  326. request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)"));
  327. try
  328. {
  329. var originals = new Dictionary<TEntity, Dictionary<string, object>>();
  330. var es = request.Items;
  331. foreach (var e in es)
  332. originals[e] = BaseObjectExtensions.GetOriginaValues(e);
  333. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
  334. store.Save(es, request.AuditNote);
  335. if (request.ReturnOnlyChanged)
  336. {
  337. for (int i = 0; i < es.Length; ++i)
  338. {
  339. var result = new Dictionary<string, object>();
  340. var e = es[i];
  341. var currentChanges = BaseObjectExtensions.GetOriginaValues(e);
  342. var preSaveChanges = originals[e];
  343. foreach (var (key, value) in currentChanges)
  344. {
  345. if (preSaveChanges.TryGetValue(key, out var oldValue))
  346. {
  347. if (!Equals(value, oldValue))
  348. {
  349. result[key] = CoreUtils.GetPropertyValue(e, key);
  350. }
  351. }
  352. else
  353. {
  354. result[key] = CoreUtils.GetPropertyValue(e, key);
  355. }
  356. }
  357. response.ChangedValues.Add(result);
  358. }
  359. }
  360. else
  361. {
  362. response.Items = es;
  363. }
  364. response.Status = StatusCode.OK;
  365. logger.Send(LogType.Information, userid,
  366. string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
  367. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), es.Length));
  368. CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
  369. }
  370. catch (Exception err)
  371. {
  372. response.Status = StatusCode.Error;
  373. response.Messages.Add(err.Message);
  374. logger.Send(LogType.Error, userid,
  375. string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
  376. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
  377. }
  378. GarbageCollection();
  379. return response;
  380. }
  381. public static DeleteResponse<TEntity> Delete(DeleteRequest<TEntity> request, Logger logger)
  382. {
  383. var start = DateTime.Now;
  384. var response = new DeleteResponse<TEntity>();
  385. var userguid = ValidateRequest(request, out var userid);
  386. if (userguid == Guid.Empty)
  387. return response.Status(StatusCode.Unauthenticated);
  388. logger.Send(LogType.Information, userid,
  389. string.Format("[{0} {1}] Delete{2} Data=[{3}]", request.Credentials.Platform, request.Credentials.Version,
  390. SimpleName(typeof(TEntity)),
  391. request.Item));
  392. try
  393. {
  394. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
  395. store.Delete(request.Item, request.AuditNote);
  396. response.Status = StatusCode.OK;
  397. logger.Send(LogType.Information, userid,
  398. string.Format("[{0} {1}] [{2:D8}] Delete{3} Complete", request.Credentials.Platform, request.Credentials.Version,
  399. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity))));
  400. CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
  401. }
  402. catch (Exception err)
  403. {
  404. response.Status = StatusCode.Error;
  405. response.Messages.Add(err.Message);
  406. logger.Send(LogType.Error, userid,
  407. string.Format("[{0} {1}] [{2:D8}] Delete{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
  408. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
  409. }
  410. GarbageCollection();
  411. return response;
  412. }
  413. public static MultiDeleteResponse<TEntity> MultiDelete(MultiDeleteRequest<TEntity> request, Logger logger)
  414. {
  415. var start = DateTime.Now;
  416. var response = new MultiDeleteResponse<TEntity>();
  417. var userguid = ValidateRequest(request, out var userid);
  418. if (userguid == Guid.Empty)
  419. return response.Status(StatusCode.Unauthenticated);
  420. logger.Send(LogType.Information, userid,
  421. string.Format("[{0} {1}] MultiDelete{2}({3}) Data=[{4}]", request.Credentials.Platform, request.Credentials.Version,
  422. SimpleName(typeof(TEntity)), request.Items.Length,
  423. request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)"));
  424. try
  425. {
  426. var es = request.Items;
  427. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
  428. store.Delete(es, request.AuditNote);
  429. response.Status = StatusCode.OK;
  430. logger.Send(LogType.Information, userid,
  431. string.Format("[{0} {1}] [{2:D8}] MultiDelete{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
  432. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), request.Items.Length));
  433. CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
  434. }
  435. catch (Exception err)
  436. {
  437. response.Status = StatusCode.Error;
  438. response.Messages.Add(err.Message);
  439. logger.Send(LogType.Error, userid,
  440. string.Format("[{0} {1}] [{2:D8}] MultiDelete{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
  441. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
  442. }
  443. GarbageCollection();
  444. return response;
  445. }
  446. }
  447. }