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.

database.py 7.6 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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.
  59. Return its ID as an integer and creation timestamp as a naive UTC
  60. datetime.
  61. """
  62. created = datetime.utcnow().replace(microsecond=0)
  63. query = "INSERT INTO session (session_created) VALUES (?)"
  64. with self._conn as conn:
  65. cur = conn.execute(query, (created,))
  66. return cur.lastrowid, created
  67. def has_session(self, sid):
  68. """Return the creation timestamp for the given session ID, or None.
  69. Will only return a timestamp for non-expired sessions. This function
  70. randomly does database maintenance; very old expired sessions may be
  71. cleared.
  72. """
  73. if random.random() <= 0.2:
  74. self._clear_old_sessions()
  75. query = """SELECT session_created FROM session
  76. WHERE session_id = ? AND
  77. strftime("%s", "now") - strftime("%s", session_created) < {} AND
  78. strftime("%s", "now") - strftime("%s", session_touched) < {}"""
  79. query = query.format(self.MAX_SESSION_AGE, self.MAX_SESSION_STALENESS)
  80. res = self._conn.execute(query, (sid,)).fetchall()
  81. if not res:
  82. return None
  83. return datetime.strptime(res[0][0], "%Y-%m-%d %H:%M:%S")
  84. def read_session(self, sid):
  85. """Return the character associated with the given session, or None."""
  86. query = """SELECT session_character FROM session
  87. WHERE session_id = ? AND
  88. strftime("%s", "now") - strftime("%s", session_created) < {} AND
  89. strftime("%s", "now") - strftime("%s", session_touched) < {}"""
  90. query = query.format(self.MAX_SESSION_AGE, self.MAX_SESSION_STALENESS)
  91. res = self._conn.execute(query, (sid,)).fetchall()
  92. return res[0][0] if res else None
  93. def touch_session(self, sid):
  94. """Update the given session's last access timestamp."""
  95. query = """UPDATE session
  96. SET session_touched = CURRENT_TIMESTAMP
  97. WHERE session_id = ?"""
  98. with self._conn as conn:
  99. conn.execute(query, (sid,))
  100. def attach_session(self, sid, cid):
  101. """Attach the given session to a character. Does not touch it."""
  102. query = """UPDATE session
  103. SET session_character = ?
  104. WHERE session_id = ?"""
  105. with self._conn as conn:
  106. conn.execute(query, (cid, sid))
  107. def drop_session(self, sid):
  108. """Remove the given session from the database."""
  109. with self._conn as conn:
  110. conn.execute("DELETE FROM session WHERE session_id = ?", (sid,))
  111. def put_character(self, cid, name):
  112. """Put a character into the database if they don't already exist."""
  113. with self._conn as conn:
  114. cur = conn.execute("BEGIN TRANSACTION")
  115. cur.execute(
  116. """UPDATE character SET character_name = ?
  117. WHERE character_id = ?""", (name, cid))
  118. if cur.rowcount == 0:
  119. cur.execute(
  120. """INSERT INTO character (character_id, character_name)
  121. VALUES (?, ?)""", (cid, name))
  122. def read_character(self, cid):
  123. """Return a dictionary of properties for the given character."""
  124. query = """SELECT character_name, character_style
  125. FROM character WHERE character_id = ?"""
  126. res = self._conn.execute(query, (cid,)).fetchall()
  127. return {"name": res[0][0], "style": res[0][1]} if res else {}
  128. def update_character(self, cid, prop, value):
  129. """Update a property for the given character."""
  130. props = {"name": "character_name", "style": "character_style"}
  131. field = props[prop]
  132. with self._conn as conn:
  133. conn.execute("""UPDATE character SET {} = ?
  134. WHERE character_id = ?""".format(field), (value, cid))
  135. def set_auth(self, cid, token, expires, refresh):
  136. """Set the authentication info for the given character."""
  137. with self._conn as conn:
  138. conn.execute("""INSERT OR REPLACE INTO auth
  139. (auth_character, auth_token, auth_token_expiry, auth_refresh)
  140. VALUES (?, ?, ?, ?)""", (cid, token, expires, refresh))
  141. def update_auth(self, cid, token, expires, refresh):
  142. """Update the authentication info for the given character.
  143. Functionally equivalent to set_auth provided that the character has an
  144. existing auth entry, but is more efficient.
  145. """
  146. with self._conn as conn:
  147. conn.execute("""UPDATE auth
  148. SET auth_token = ?, auth_token_expiry = ?, auth_refresh = ?
  149. WHERE auth_character = ?""", (token, expires, refresh, cid))
  150. def get_auth(self, cid):
  151. """Return authentication info for the given character.
  152. Return a 3-tuple of (access_token, token_expiry, refresh_token), or
  153. None if there is no auth info.
  154. """
  155. query = """SELECT auth_token, auth_token_expiry, auth_refresh
  156. FROM auth WHERE auth_character = ?"""
  157. res = self._conn.execute(query, (cid,)).fetchall()
  158. if not res:
  159. return None
  160. token, expiry, refresh = res[0]
  161. expires = datetime.strptime(expiry, "%Y-%m-%d %H:%M:%S")
  162. return token, expires, refresh
  163. def drop_auth(self, cid):
  164. """Drop any authentication info for the given character."""
  165. with self._conn as conn:
  166. conn.execute("DELETE FROM auth WHERE auth_character = ?", (cid,))