A corporation manager and dashboard for EVE Online
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

197 lines
7.2 KiB

  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime
  3. import random
  4. import sqlite3
  5. from flask import g
  6. from werkzeug.local import LocalProxy
  7. __all__ = ["Database"]
  8. class Database:
  9. """Database manager for low-level authentication actions."""
  10. MAX_SESSION_STALENESS = 2 * 60 * 60 # 2 hours
  11. MAX_SESSION_AGE = 24 * 60 * 60 # 24 hours
  12. SESSION_GRACE = 60 * 60 # 1 hour
  13. path = None
  14. def __init__(self):
  15. if self.path is None:
  16. raise RuntimeError("Database.path not set")
  17. self._conn = sqlite3.connect(self.path)
  18. def __enter__(self):
  19. return self._conn.__enter__()
  20. def __exit__(self, exc_type, exc_value, trace):
  21. return self._conn.__exit__(exc_type, exc_value, trace)
  22. @classmethod
  23. def _get(cls):
  24. """Return the current database, or allocate a new one if necessary."""
  25. if not hasattr(g, "_db"):
  26. g._db = cls()
  27. return g._db
  28. @classmethod
  29. def pre_hook(cls):
  30. """Hook to be called before a request context.
  31. Sets up the g.db proxy.
  32. """
  33. g.db = LocalProxy(cls._get)
  34. @classmethod
  35. def post_hook(cls, exc):
  36. """Hook to be called when tearing down an application context.
  37. Closes the database if necessary.
  38. """
  39. if hasattr(g, "_db"):
  40. g._db.close()
  41. def close(self):
  42. """Close the database connection."""
  43. return self._conn.close()
  44. def _clear_old_sessions(self):
  45. """Remove old sessions from the database.
  46. Sessions can expire if they are not touched (accessed) in a certain
  47. period of time, or if their absolute age exceeds some number. We don't
  48. actually remove them until a bit after this time.
  49. """
  50. query = """DELETE FROM session WHERE
  51. strftime("%s", "now") - strftime("%s", session_created) >= {} OR
  52. strftime("%s", "now") - strftime("%s", session_touched) >= {}"""
  53. create_thresh = self.MAX_SESSION_AGE + self.SESSION_GRACE
  54. touch_thresh = self.MAX_SESSION_STALENESS + self.SESSION_GRACE
  55. with self._conn as conn:
  56. conn.execute(query.format(create_thresh, touch_thresh))
  57. def new_session(self):
  58. """Allocate a new session in the database and return its ID."""
  59. with self._conn as conn:
  60. cur = conn.execute("INSERT INTO session DEFAULT VALUES")
  61. return cur.lastrowid
  62. def has_session(self, sid):
  63. """Return whether the given session ID exists in the database.
  64. Will only be True for non-expired sessions. This function randomly does
  65. database maintenance; very old expired sessions may be cleared.
  66. """
  67. if random.random() <= 0.2:
  68. self._clear_old_sessions()
  69. query = """SELECT 1 FROM session
  70. WHERE session_id = ? AND
  71. strftime("%s", "now") - strftime("%s", session_created) < {} AND
  72. strftime("%s", "now") - strftime("%s", session_touched) < {}"""
  73. query = query.format(self.MAX_SESSION_AGE, self.MAX_SESSION_STALENESS)
  74. cur = self._conn.execute(query, (sid,))
  75. return bool(cur.fetchall())
  76. def read_session(self, sid):
  77. """Return the character associated with the given session, or None."""
  78. query = """SELECT session_character FROM session
  79. WHERE session_id = ? AND
  80. strftime("%s", "now") - strftime("%s", session_created) < {} AND
  81. strftime("%s", "now") - strftime("%s", session_touched) < {}"""
  82. query = query.format(self.MAX_SESSION_AGE, self.MAX_SESSION_STALENESS)
  83. res = self._conn.execute(query, (sid,)).fetchall()
  84. return res[0][0] if res else None
  85. def touch_session(self, sid):
  86. """Update the given session's last access timestamp."""
  87. query = """UPDATE session
  88. SET session_touched = CURRENT_TIMESTAMP
  89. WHERE session_id = ?"""
  90. with self._conn as conn:
  91. conn.execute(query, (sid,))
  92. def attach_session(self, sid, cid):
  93. """Attach the given session to a character. Does not touch it."""
  94. query = """UPDATE session
  95. SET session_character = ?
  96. WHERE session_id = ?"""
  97. with self._conn as conn:
  98. conn.execute(query, (cid, sid))
  99. def drop_session(self, sid):
  100. """Remove the given session from the database."""
  101. with self._conn as conn:
  102. conn.execute("DELETE FROM session WHERE session_id = ?", (sid,))
  103. def put_character(self, cid, name):
  104. """Put a character into the database if they don't already exist."""
  105. with self._conn as conn:
  106. cur = conn.execute("BEGIN TRANSACTION")
  107. cur.execute(
  108. """UPDATE character SET character_name = ?
  109. WHERE character_id = ?""", (name, cid))
  110. if cur.rowcount == 0:
  111. cur.execute(
  112. """INSERT INTO character (character_id, character_name)
  113. VALUES (?, ?)""", (cid, name))
  114. def read_character(self, cid):
  115. """Return a dictionary of properties for the given character."""
  116. query = """SELECT character_name, character_style
  117. FROM character WHERE character_id = ?"""
  118. res = self._conn.execute(query, (cid,)).fetchall()
  119. return {"name": res[0][0], "style": res[0][1]} if res else {}
  120. def update_character(self, cid, prop, value):
  121. """Update a property for the given character."""
  122. props = {"name": "character_name", "style": "character_style"}
  123. field = props[prop]
  124. with self._conn as conn:
  125. conn.execute("""UPDATE character SET {} = ?
  126. WHERE character_id = ?""".format(field), (value, cid))
  127. def set_auth(self, cid, token, expires, refresh):
  128. """Set the authentication info for the given character."""
  129. with self._conn as conn:
  130. conn.execute("""INSERT OR REPLACE INTO auth
  131. (auth_character, auth_token, auth_token_expiry, auth_refresh)
  132. VALUES (?, ?, ?, ?)""", (cid, token, expires, refresh))
  133. def update_auth(self, cid, token, expires, refresh):
  134. """Update the authentication info for the given character.
  135. Functionally equivalent to set_auth provided that the character has an
  136. existing auth entry, but is more efficient.
  137. """
  138. with self._conn as conn:
  139. conn.execute("""UPDATE auth
  140. SET auth_token = ?, auth_token_expiry = ?, auth_refresh = ?
  141. WHERE auth_character = ?""", (token, expires, refresh, cid))
  142. def get_auth(self, cid):
  143. """Return authentication info for the given character.
  144. Return a 3-tuple of (access_token, token_expiry, refresh_token), or
  145. None if there is no auth info.
  146. """
  147. query = """SELECT auth_token, auth_token_expiry, auth_refresh
  148. FROM auth WHERE auth_character = ?"""
  149. res = self._conn.execute(query, (cid,)).fetchall()
  150. if not res:
  151. return None
  152. token, expiry, refresh = res[0]
  153. expires = datetime.strptime(expiry, "%Y-%m-%d %H:%M:%S.%f")
  154. return token, expires, refresh
  155. def drop_auth(self, cid):
  156. """Drop any authentication info for the given character."""
  157. with self._conn as conn:
  158. conn.execute("DELETE FROM auth WHERE auth_character = ?", (cid,))