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.
 
 
 
 
 

244 lines
9.1 KiB

  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime
  3. import random
  4. import sqlite3
  5. from flask import current_app, 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 = 7 * 24 * 60 * 60 # 7 days
  11. MAX_SESSION_AGE = 30 * 24 * 60 * 60 # 30 days
  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. if current_app.debug:
  51. return # Sessions don't expire in debug mode
  52. query = """DELETE FROM session WHERE
  53. strftime("%s", "now") - strftime("%s", session_created) >= {} OR
  54. strftime("%s", "now") - strftime("%s", session_touched) >= {}"""
  55. create_thresh = self.MAX_SESSION_AGE + self.SESSION_GRACE
  56. touch_thresh = self.MAX_SESSION_STALENESS + self.SESSION_GRACE
  57. with self._conn as conn:
  58. conn.execute(query.format(create_thresh, touch_thresh))
  59. def _build_expiry_check(self):
  60. """Build and return a snippet of SQL to check for valid sessions.
  61. The SQL should be inserted in a WHERE clause. If debug mode is active,
  62. we just return an empty string.
  63. """
  64. if current_app.debug:
  65. return ""
  66. check = """ AND
  67. strftime("%s", "now") -
  68. strftime("%s", session_created) < {} AND
  69. strftime("%s", "now") -
  70. strftime("%s", session_touched) < {}"""
  71. return check.format(self.MAX_SESSION_AGE, self.MAX_SESSION_STALENESS)
  72. def new_session(self):
  73. """Allocate a new session in the database.
  74. Return its ID as an integer and creation timestamp as a naive UTC
  75. datetime.
  76. """
  77. created = datetime.utcnow().replace(microsecond=0)
  78. query = "INSERT INTO session (session_created) VALUES (?)"
  79. with self._conn as conn:
  80. cur = conn.execute(query, (created,))
  81. return cur.lastrowid, created
  82. def has_session(self, sid):
  83. """Return the creation timestamp for the given session ID, or None.
  84. Will only return a timestamp for non-expired sessions. This function
  85. randomly does database maintenance; very old expired sessions may be
  86. cleared.
  87. """
  88. if random.random() <= 0.2:
  89. self._clear_old_sessions()
  90. query = """SELECT session_created FROM session
  91. WHERE session_id = ?""" + self._build_expiry_check()
  92. res = self._conn.execute(query, (sid,)).fetchall()
  93. if not res:
  94. return None
  95. return datetime.strptime(res[0][0], "%Y-%m-%d %H:%M:%S")
  96. def read_session(self, sid):
  97. """Return the character associated with the given session, or None."""
  98. query = """SELECT session_character FROM session
  99. WHERE session_id = ?""" + self._build_expiry_check()
  100. res = self._conn.execute(query, (sid,)).fetchall()
  101. return res[0][0] if res else None
  102. def touch_session(self, sid):
  103. """Update the given session's last access timestamp."""
  104. query = """UPDATE session
  105. SET session_touched = CURRENT_TIMESTAMP
  106. WHERE session_id = ?"""
  107. with self._conn as conn:
  108. conn.execute(query, (sid,))
  109. def attach_session(self, sid, cid):
  110. """Attach the given session to a character. Does not touch it."""
  111. query = """UPDATE session
  112. SET session_character = ?
  113. WHERE session_id = ?"""
  114. with self._conn as conn:
  115. conn.execute(query, (cid, sid))
  116. def drop_session(self, sid):
  117. """Remove the given session from the database."""
  118. with self._conn as conn:
  119. conn.execute("DELETE FROM session WHERE session_id = ?", (sid,))
  120. def put_character(self, cid, name):
  121. """Put a character into the database if they don't already exist."""
  122. with self._conn as conn:
  123. cur = conn.execute("BEGIN TRANSACTION")
  124. cur.execute(
  125. """UPDATE character SET character_name = ?
  126. WHERE character_id = ?""", (name, cid))
  127. if cur.rowcount == 0:
  128. cur.execute(
  129. """INSERT INTO character (character_id, character_name)
  130. VALUES (?, ?)""", (cid, name))
  131. def read_character(self, cid):
  132. """Return a dictionary of properties for the given character."""
  133. query = """SELECT character_name, character_style
  134. FROM character WHERE character_id = ?"""
  135. res = self._conn.execute(query, (cid,)).fetchall()
  136. return {"name": res[0][0], "style": res[0][1]} if res else {}
  137. def update_character(self, cid, prop, value):
  138. """Update a property for the given character."""
  139. props = {"name": "character_name", "style": "character_style"}
  140. field = props[prop]
  141. with self._conn as conn:
  142. conn.execute("""UPDATE character SET {} = ?
  143. WHERE character_id = ?""".format(field), (value, cid))
  144. def set_auth(self, cid, token, expires, refresh):
  145. """Set the authentication info for the given character."""
  146. with self._conn as conn:
  147. conn.execute("""INSERT OR REPLACE INTO auth
  148. (auth_character, auth_token, auth_token_expiry, auth_refresh)
  149. VALUES (?, ?, ?, ?)""", (cid, token, expires, refresh))
  150. def update_auth(self, cid, token, expires, refresh):
  151. """Update the authentication info for the given character.
  152. Functionally equivalent to set_auth provided that the character has an
  153. existing auth entry, but is more efficient.
  154. """
  155. with self._conn as conn:
  156. conn.execute("""UPDATE auth
  157. SET auth_token = ?, auth_token_expiry = ?, auth_refresh = ?
  158. WHERE auth_character = ?""", (token, expires, refresh, cid))
  159. def get_auth(self, cid):
  160. """Return authentication info for the given character.
  161. Return a 3-tuple of (access_token, token_expiry, refresh_token), or
  162. None if there is no auth info.
  163. """
  164. query = """SELECT auth_token, auth_token_expiry, auth_refresh
  165. FROM auth WHERE auth_character = ?"""
  166. res = self._conn.execute(query, (cid,)).fetchall()
  167. if not res:
  168. return None
  169. token, expiry, refresh = res[0]
  170. expires = datetime.strptime(expiry, "%Y-%m-%d %H:%M:%S")
  171. return token, expires, refresh
  172. def drop_auth(self, cid):
  173. """Drop any authentication info for the given character."""
  174. with self._conn as conn:
  175. conn.execute("DELETE FROM auth WHERE auth_character = ?", (cid,))
  176. def get_authed_characters(self):
  177. """Return a list of characters with authentication info.
  178. Each list item is a 4-tuple of (character_id, access_token,
  179. token_expiry, refresh_token).
  180. """
  181. query = """SELECT auth_character, auth_token, auth_token_expiry,
  182. auth_refresh FROM auth"""
  183. res = self._conn.execute(query).fetchall()
  184. dtparse = lambda dt: datetime.strptime(dt, "%Y-%m-%d %H:%M:%S")
  185. return [(cid, token, dtparse(expiry), refresh)
  186. for (cid, token, expiry, refresh) in res]
  187. def set_character_modprop(self, cid, module, prop, value):
  188. """Add or update a character module property."""
  189. with self._conn as conn:
  190. conn.execute("""INSERT OR REPLACE INTO character_prop
  191. (cprop_character, cprop_module, cprop_key, cprop_value)
  192. VALUES (?, ?, ?, ?)""", (cid, module, prop, value))
  193. def get_character_modprop(self, cid, module, prop):
  194. """Return the value of a character module property, or None."""
  195. query = """SELECT cprop_value FROM character_prop
  196. WHERE cprop_character = ? AND cprop_module = ? AND cprop_key = ?"""
  197. res = self._conn.execute(query, (cid, module, prop)).fetchall()
  198. return res[0][0] if res else None