1
0

ServerDbSqlite.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. using System.Collections.Immutable;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Runtime.CompilerServices;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using Content.Server.Administration.Logs;
  9. using Content.Server.IP;
  10. using Content.Server.Preferences.Managers;
  11. using Content.Shared.CCVar;
  12. using Content.Shared.Database;
  13. using Microsoft.EntityFrameworkCore;
  14. using Robust.Shared.Configuration;
  15. using Robust.Shared.Network;
  16. using Robust.Shared.Utility;
  17. namespace Content.Server.Database
  18. {
  19. /// <summary>
  20. /// Provides methods to retrieve and update character preferences.
  21. /// Don't use this directly, go through <see cref="ServerPreferencesManager" /> instead.
  22. /// </summary>
  23. public sealed class ServerDbSqlite : ServerDbBase
  24. {
  25. private readonly Func<DbContextOptions<SqliteServerDbContext>> _options;
  26. private readonly ConcurrencySemaphore _prefsSemaphore;
  27. private readonly Task _dbReadyTask;
  28. private int _msDelay;
  29. public ServerDbSqlite(
  30. Func<DbContextOptions<SqliteServerDbContext>> options,
  31. bool inMemory,
  32. IConfigurationManager cfg,
  33. bool synchronous,
  34. ISawmill opsLog)
  35. : base(opsLog)
  36. {
  37. _options = options;
  38. var prefsCtx = new SqliteServerDbContext(options());
  39. // When inMemory we re-use the same connection, so we can't have any concurrency.
  40. var concurrency = inMemory ? 1 : cfg.GetCVar(CCVars.DatabaseSqliteConcurrency);
  41. _prefsSemaphore = new ConcurrencySemaphore(concurrency, synchronous);
  42. if (synchronous)
  43. {
  44. prefsCtx.Database.Migrate();
  45. _dbReadyTask = Task.CompletedTask;
  46. prefsCtx.Dispose();
  47. }
  48. else
  49. {
  50. _dbReadyTask = Task.Run(() =>
  51. {
  52. prefsCtx.Database.Migrate();
  53. prefsCtx.Dispose();
  54. });
  55. }
  56. cfg.OnValueChanged(CCVars.DatabaseSqliteDelay, v => _msDelay = v, true);
  57. }
  58. #region Ban
  59. public override async Task<ServerBanDef?> GetServerBanAsync(int id)
  60. {
  61. await using var db = await GetDbImpl();
  62. var ban = await db.SqliteDbContext.Ban
  63. .Include(p => p.Unban)
  64. .Where(p => p.Id == id)
  65. .SingleOrDefaultAsync();
  66. return ConvertBan(ban);
  67. }
  68. public override async Task<ServerBanDef?> GetServerBanAsync(
  69. IPAddress? address,
  70. NetUserId? userId,
  71. ImmutableArray<byte>? hwId,
  72. ImmutableArray<ImmutableArray<byte>>? modernHWIds)
  73. {
  74. await using var db = await GetDbImpl();
  75. return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault();
  76. }
  77. public override async Task<List<ServerBanDef>> GetServerBansAsync(
  78. IPAddress? address,
  79. NetUserId? userId,
  80. ImmutableArray<byte>? hwId,
  81. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  82. bool includeUnbanned)
  83. {
  84. await using var db = await GetDbImpl();
  85. return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList();
  86. }
  87. private async Task<IEnumerable<ServerBanDef>> GetServerBanQueryAsync(
  88. DbGuardImpl db,
  89. IPAddress? address,
  90. NetUserId? userId,
  91. ImmutableArray<byte>? hwId,
  92. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  93. bool includeUnbanned)
  94. {
  95. var exempt = await GetBanExemptionCore(db, userId);
  96. var newPlayer = !await db.SqliteDbContext.Player.AnyAsync(p => p.UserId == userId);
  97. // SQLite can't do the net masking stuff we need to match IP address ranges.
  98. // So just pull down the whole list into memory.
  99. var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt);
  100. var playerInfo = new BanMatcher.PlayerInfo
  101. {
  102. Address = address,
  103. UserId = userId,
  104. ExemptFlags = exempt ?? default,
  105. HWId = hwId,
  106. ModernHWIds = modernHWIds,
  107. IsNewPlayer = newPlayer,
  108. };
  109. return queryBans
  110. .Select(ConvertBan)
  111. .Where(b => BanMatcher.BanMatches(b!, playerInfo))!;
  112. }
  113. private static async Task<List<ServerBan>> GetAllBans(
  114. SqliteServerDbContext db,
  115. bool includeUnbanned,
  116. ServerBanExemptFlags? exemptFlags)
  117. {
  118. IQueryable<ServerBan> query = db.Ban.Include(p => p.Unban);
  119. if (!includeUnbanned)
  120. {
  121. query = query.Where(p =>
  122. p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow));
  123. }
  124. if (exemptFlags is { } exempt)
  125. {
  126. // Any flag to bypass BlacklistedRange bans.
  127. if (exempt != ServerBanExemptFlags.None)
  128. exempt |= ServerBanExemptFlags.BlacklistedRange;
  129. query = query.Where(b => (b.ExemptFlags & exempt) == 0);
  130. }
  131. return await query.ToListAsync();
  132. }
  133. public override async Task AddServerBanAsync(ServerBanDef serverBan)
  134. {
  135. await using var db = await GetDbImpl();
  136. db.SqliteDbContext.Ban.Add(new ServerBan
  137. {
  138. Address = serverBan.Address.ToNpgsqlInet(),
  139. Reason = serverBan.Reason,
  140. Severity = serverBan.Severity,
  141. BanningAdmin = serverBan.BanningAdmin?.UserId,
  142. HWId = serverBan.HWId,
  143. BanTime = serverBan.BanTime.UtcDateTime,
  144. ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
  145. RoundId = serverBan.RoundId,
  146. PlaytimeAtNote = serverBan.PlaytimeAtNote,
  147. PlayerUserId = serverBan.UserId?.UserId,
  148. ExemptFlags = serverBan.ExemptFlags
  149. });
  150. await db.SqliteDbContext.SaveChangesAsync();
  151. }
  152. public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban)
  153. {
  154. await using var db = await GetDbImpl();
  155. db.SqliteDbContext.Unban.Add(new ServerUnban
  156. {
  157. BanId = serverUnban.BanId,
  158. UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId,
  159. UnbanTime = serverUnban.UnbanTime.UtcDateTime
  160. });
  161. await db.SqliteDbContext.SaveChangesAsync();
  162. }
  163. #endregion
  164. #region Role Ban
  165. public override async Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
  166. {
  167. await using var db = await GetDbImpl();
  168. var ban = await db.SqliteDbContext.RoleBan
  169. .Include(p => p.Unban)
  170. .Where(p => p.Id == id)
  171. .SingleOrDefaultAsync();
  172. return ConvertRoleBan(ban);
  173. }
  174. public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
  175. IPAddress? address,
  176. NetUserId? userId,
  177. ImmutableArray<byte>? hwId,
  178. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  179. bool includeUnbanned)
  180. {
  181. await using var db = await GetDbImpl();
  182. // SQLite can't do the net masking stuff we need to match IP address ranges.
  183. // So just pull down the whole list into memory.
  184. var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned);
  185. return queryBans
  186. .Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds))
  187. .Select(ConvertRoleBan)
  188. .ToList()!;
  189. }
  190. private static async Task<List<ServerRoleBan>> GetAllRoleBans(
  191. SqliteServerDbContext db,
  192. bool includeUnbanned)
  193. {
  194. IQueryable<ServerRoleBan> query = db.RoleBan.Include(p => p.Unban);
  195. if (!includeUnbanned)
  196. {
  197. query = query.Where(p =>
  198. p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow));
  199. }
  200. return await query.ToListAsync();
  201. }
  202. private static bool RoleBanMatches(
  203. ServerRoleBan ban,
  204. IPAddress? address,
  205. NetUserId? userId,
  206. ImmutableArray<byte>? hwId,
  207. ImmutableArray<ImmutableArray<byte>>? modernHWIds)
  208. {
  209. if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
  210. {
  211. return true;
  212. }
  213. if (userId is { } id && ban.PlayerUserId == id.UserId)
  214. {
  215. return true;
  216. }
  217. switch (ban.HWId?.Type)
  218. {
  219. case HwidType.Legacy:
  220. if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid))
  221. return true;
  222. break;
  223. case HwidType.Modern:
  224. if (modernHWIds != null)
  225. {
  226. foreach (var modernHWId in modernHWIds)
  227. {
  228. if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid))
  229. return true;
  230. }
  231. }
  232. break;
  233. }
  234. return false;
  235. }
  236. public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan)
  237. {
  238. await using var db = await GetDbImpl();
  239. var ban = new ServerRoleBan
  240. {
  241. Address = serverBan.Address.ToNpgsqlInet(),
  242. Reason = serverBan.Reason,
  243. Severity = serverBan.Severity,
  244. BanningAdmin = serverBan.BanningAdmin?.UserId,
  245. HWId = serverBan.HWId,
  246. BanTime = serverBan.BanTime.UtcDateTime,
  247. ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
  248. RoundId = serverBan.RoundId,
  249. PlaytimeAtNote = serverBan.PlaytimeAtNote,
  250. PlayerUserId = serverBan.UserId?.UserId,
  251. RoleId = serverBan.Role,
  252. };
  253. db.SqliteDbContext.RoleBan.Add(ban);
  254. await db.SqliteDbContext.SaveChangesAsync();
  255. return ConvertRoleBan(ban);
  256. }
  257. public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnban)
  258. {
  259. await using var db = await GetDbImpl();
  260. db.SqliteDbContext.RoleUnban.Add(new ServerRoleUnban
  261. {
  262. BanId = serverUnban.BanId,
  263. UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId,
  264. UnbanTime = serverUnban.UnbanTime.UtcDateTime
  265. });
  266. await db.SqliteDbContext.SaveChangesAsync();
  267. }
  268. [return: NotNullIfNotNull(nameof(ban))]
  269. private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban)
  270. {
  271. if (ban == null)
  272. {
  273. return null;
  274. }
  275. NetUserId? uid = null;
  276. if (ban.PlayerUserId is { } guid)
  277. {
  278. uid = new NetUserId(guid);
  279. }
  280. NetUserId? aUid = null;
  281. if (ban.BanningAdmin is { } aGuid)
  282. {
  283. aUid = new NetUserId(aGuid);
  284. }
  285. var unban = ConvertRoleUnban(ban.Unban);
  286. return new ServerRoleBanDef(
  287. ban.Id,
  288. uid,
  289. ban.Address.ToTuple(),
  290. ban.HWId,
  291. // SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
  292. DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
  293. ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
  294. ban.RoundId,
  295. ban.PlaytimeAtNote,
  296. ban.Reason,
  297. ban.Severity,
  298. aUid,
  299. unban,
  300. ban.RoleId);
  301. }
  302. private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban)
  303. {
  304. if (unban == null)
  305. {
  306. return null;
  307. }
  308. NetUserId? aUid = null;
  309. if (unban.UnbanningAdmin is { } aGuid)
  310. {
  311. aUid = new NetUserId(aGuid);
  312. }
  313. return new ServerRoleUnbanDef(
  314. unban.Id,
  315. aUid,
  316. // SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
  317. DateTime.SpecifyKind(unban.UnbanTime, DateTimeKind.Utc));
  318. }
  319. #endregion
  320. [return: NotNullIfNotNull(nameof(ban))]
  321. private static ServerBanDef? ConvertBan(ServerBan? ban)
  322. {
  323. if (ban == null)
  324. {
  325. return null;
  326. }
  327. NetUserId? uid = null;
  328. if (ban.PlayerUserId is { } guid)
  329. {
  330. uid = new NetUserId(guid);
  331. }
  332. NetUserId? aUid = null;
  333. if (ban.BanningAdmin is { } aGuid)
  334. {
  335. aUid = new NetUserId(aGuid);
  336. }
  337. var unban = ConvertUnban(ban.Unban);
  338. return new ServerBanDef(
  339. ban.Id,
  340. uid,
  341. ban.Address.ToTuple(),
  342. ban.HWId,
  343. // SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
  344. DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
  345. ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
  346. ban.RoundId,
  347. ban.PlaytimeAtNote,
  348. ban.Reason,
  349. ban.Severity,
  350. aUid,
  351. unban);
  352. }
  353. private static ServerUnbanDef? ConvertUnban(ServerUnban? unban)
  354. {
  355. if (unban == null)
  356. {
  357. return null;
  358. }
  359. NetUserId? aUid = null;
  360. if (unban.UnbanningAdmin is { } aGuid)
  361. {
  362. aUid = new NetUserId(aGuid);
  363. }
  364. return new ServerUnbanDef(
  365. unban.Id,
  366. aUid,
  367. // SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
  368. DateTime.SpecifyKind(unban.UnbanTime, DateTimeKind.Utc));
  369. }
  370. public override async Task<int> AddConnectionLogAsync(
  371. NetUserId userId,
  372. string userName,
  373. IPAddress address,
  374. ImmutableTypedHwid? hwId,
  375. float trust,
  376. ConnectionDenyReason? denied,
  377. int serverId)
  378. {
  379. await using var db = await GetDbImpl();
  380. var connectionLog = new ConnectionLog
  381. {
  382. Address = address,
  383. Time = DateTime.UtcNow,
  384. UserId = userId.UserId,
  385. UserName = userName,
  386. HWId = hwId,
  387. Denied = denied,
  388. ServerId = serverId,
  389. Trust = trust,
  390. };
  391. db.SqliteDbContext.ConnectionLog.Add(connectionLog);
  392. await db.SqliteDbContext.SaveChangesAsync();
  393. return connectionLog.Id;
  394. }
  395. public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
  396. CancellationToken cancel)
  397. {
  398. await using var db = await GetDbImpl(cancel);
  399. var admins = await db.SqliteDbContext.Admin
  400. .Include(a => a.Flags)
  401. .GroupJoin(db.SqliteDbContext.Player, a => a.UserId, p => p.UserId, (a, grouping) => new {a, grouping})
  402. .SelectMany(t => t.grouping.DefaultIfEmpty(), (t, p) => new {t.a, p!.LastSeenUserName})
  403. .ToArrayAsync(cancel);
  404. var adminRanks = await db.DbContext.AdminRank.Include(a => a.Flags).ToArrayAsync(cancel);
  405. return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!;
  406. }
  407. protected override IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null)
  408. {
  409. IQueryable<AdminLog> query = db.AdminLog;
  410. if (filter?.Search != null)
  411. query = query.Where(log => EF.Functions.Like(log.Message, $"%{filter.Search}%"));
  412. return query;
  413. }
  414. public override async Task<int> AddAdminNote(AdminNote note)
  415. {
  416. await using (var db = await GetDb())
  417. {
  418. var nextId = 1;
  419. if (await db.DbContext.AdminNotes.AnyAsync())
  420. {
  421. nextId = await db.DbContext.AdminNotes.MaxAsync(adminNote => adminNote.Id) + 1;
  422. }
  423. note.Id = nextId;
  424. }
  425. return await base.AddAdminNote(note);
  426. }
  427. public override async Task<int> AddAdminWatchlist(AdminWatchlist watchlist)
  428. {
  429. await using (var db = await GetDb())
  430. {
  431. var nextId = 1;
  432. if (await db.DbContext.AdminWatchlists.AnyAsync())
  433. {
  434. nextId = await db.DbContext.AdminWatchlists.MaxAsync(adminWatchlist => adminWatchlist.Id) + 1;
  435. }
  436. watchlist.Id = nextId;
  437. }
  438. return await base.AddAdminWatchlist(watchlist);
  439. }
  440. public override async Task<int> AddAdminMessage(AdminMessage message)
  441. {
  442. await using (var db = await GetDb())
  443. {
  444. var nextId = 1;
  445. if (await db.DbContext.AdminMessages.AnyAsync())
  446. {
  447. nextId = await db.DbContext.AdminMessages.MaxAsync(adminMessage => adminMessage.Id) + 1;
  448. }
  449. message.Id = nextId;
  450. }
  451. return await base.AddAdminMessage(message);
  452. }
  453. public override Task SendNotification(DatabaseNotification notification)
  454. {
  455. // Notifications not implemented on SQLite.
  456. return Task.CompletedTask;
  457. }
  458. protected override DateTime NormalizeDatabaseTime(DateTime time)
  459. {
  460. DebugTools.Assert(time.Kind == DateTimeKind.Unspecified);
  461. return DateTime.SpecifyKind(time, DateTimeKind.Utc);
  462. }
  463. private async Task<DbGuardImpl> GetDbImpl(
  464. CancellationToken cancel = default,
  465. [CallerMemberName] string? name = null)
  466. {
  467. LogDbOp(name);
  468. await _dbReadyTask;
  469. if (_msDelay > 0)
  470. await Task.Delay(_msDelay, cancel);
  471. await _prefsSemaphore.WaitAsync(cancel);
  472. var dbContext = new SqliteServerDbContext(_options());
  473. return new DbGuardImpl(this, dbContext);
  474. }
  475. protected override async Task<DbGuard> GetDb(
  476. CancellationToken cancel = default,
  477. [CallerMemberName] string? name = null)
  478. {
  479. return await GetDbImpl(cancel, name).ConfigureAwait(false);
  480. }
  481. private sealed class DbGuardImpl : DbGuard
  482. {
  483. private readonly ServerDbSqlite _db;
  484. private readonly SqliteServerDbContext _ctx;
  485. public DbGuardImpl(ServerDbSqlite db, SqliteServerDbContext dbContext)
  486. {
  487. _db = db;
  488. _ctx = dbContext;
  489. }
  490. public override ServerDbContext DbContext => _ctx;
  491. public SqliteServerDbContext SqliteDbContext => _ctx;
  492. public override async ValueTask DisposeAsync()
  493. {
  494. await _ctx.DisposeAsync();
  495. _db._prefsSemaphore.Release();
  496. }
  497. }
  498. private sealed class ConcurrencySemaphore
  499. {
  500. private readonly bool _synchronous;
  501. private readonly SemaphoreSlim _semaphore;
  502. private Thread? _holdingThread;
  503. public ConcurrencySemaphore(int maxCount, bool synchronous)
  504. {
  505. if (synchronous && maxCount != 1)
  506. throw new ArgumentException("If synchronous, max concurrency must be 1");
  507. _synchronous = synchronous;
  508. _semaphore = new SemaphoreSlim(maxCount, maxCount);
  509. }
  510. public Task WaitAsync(CancellationToken cancel = default)
  511. {
  512. var task = _semaphore.WaitAsync(cancel);
  513. if (_synchronous)
  514. {
  515. if (!task.IsCompleted)
  516. {
  517. if (Thread.CurrentThread == _holdingThread)
  518. {
  519. throw new InvalidOperationException(
  520. "Multiple database requests from same thread on synchronous database!");
  521. }
  522. throw new InvalidOperationException(
  523. $"Different threads trying to access the database at once! " +
  524. $"Holding thread: {DiagThread(_holdingThread)}, " +
  525. $"current thread: {DiagThread(Thread.CurrentThread)}");
  526. }
  527. _holdingThread = Thread.CurrentThread;
  528. }
  529. return task;
  530. }
  531. public void Release()
  532. {
  533. if (_synchronous)
  534. {
  535. if (Thread.CurrentThread != _holdingThread)
  536. throw new InvalidOperationException("Released on different thread than took lock???");
  537. _holdingThread = null;
  538. }
  539. _semaphore.Release();
  540. }
  541. private static string DiagThread(Thread? thread)
  542. {
  543. if (thread != null)
  544. return $"{thread.Name} ({thread.ManagedThreadId})";
  545. return "<null thread>";
  546. }
  547. }
  548. }
  549. }