First git commit
This commit is contained in:
126
public_html/bbs/bbsdb.py
Normal file
126
public_html/bbs/bbsdb.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""Shared data layer for the txt3 BBS (telnet front-end).
|
||||
|
||||
Talks to the exact same SQLite database as the WAP/XHTML site, so accounts,
|
||||
mail, forums and scores are identical across all three front-ends.
|
||||
|
||||
Password hashes are PHP `password_hash()` bcrypt ($2y$) values. Rather than
|
||||
depend on a bcrypt wheel, we verify/create them by calling the php binary,
|
||||
passing values through the environment so nothing is ever interpolated into
|
||||
a shell string.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
DB_PATH = os.environ.get(
|
||||
"BBS_DB", "/home/txt3/domains/wap.txt3.net/public_html/data/wap.sqlite"
|
||||
)
|
||||
PHP_BIN = os.environ.get("BBS_PHP", "/usr/bin/php")
|
||||
|
||||
|
||||
def connect():
|
||||
con = sqlite3.connect(DB_PATH, timeout=10)
|
||||
con.row_factory = sqlite3.Row
|
||||
con.execute("PRAGMA journal_mode = WAL")
|
||||
con.execute("PRAGMA foreign_keys = ON")
|
||||
con.execute("PRAGMA busy_timeout = 5000")
|
||||
return con
|
||||
|
||||
|
||||
def q(con, sql, args=()):
|
||||
return con.execute(sql, args).fetchall()
|
||||
|
||||
|
||||
def q1(con, sql, args=()):
|
||||
r = con.execute(sql, args).fetchone()
|
||||
return r
|
||||
|
||||
|
||||
def ex(con, sql, args=()):
|
||||
cur = con.execute(sql, args)
|
||||
con.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- passwords
|
||||
def _php(code, env):
|
||||
e = dict(os.environ)
|
||||
e.update(env)
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[PHP_BIN, "-r", code],
|
||||
capture_output=True, text=True, timeout=10, env=e,
|
||||
)
|
||||
return out.stdout.strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def verify_password(plain, hashed):
|
||||
return _php(
|
||||
'echo password_verify(getenv("BP_P"), getenv("BP_H")) ? "1" : "0";',
|
||||
{"BP_P": plain, "BP_H": hashed},
|
||||
) == "1"
|
||||
|
||||
|
||||
def hash_password(plain):
|
||||
h = _php(
|
||||
'echo password_hash(getenv("BP_P"), PASSWORD_DEFAULT);',
|
||||
{"BP_P": plain},
|
||||
)
|
||||
return h or None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- users
|
||||
def user_by_name(con, name):
|
||||
return q1(con, "SELECT * FROM users WHERE username=? COLLATE NOCASE", (name,))
|
||||
|
||||
|
||||
def user_by_id(con, uid):
|
||||
return q1(con, "SELECT * FROM users WHERE id=?", (uid,))
|
||||
|
||||
|
||||
def login(con, name, password):
|
||||
u = user_by_name(con, name)
|
||||
if not u or u["is_banned"]:
|
||||
return None
|
||||
if not verify_password(password, u["pass_hash"]):
|
||||
return None
|
||||
ex(con, "UPDATE users SET last_seen=? WHERE id=?", (int(time.time()), u["id"]))
|
||||
return u
|
||||
|
||||
|
||||
def create_user(con, name, password):
|
||||
h = hash_password(password)
|
||||
if not h:
|
||||
return None
|
||||
now = int(time.time())
|
||||
uid = ex(
|
||||
con,
|
||||
"INSERT INTO users (username,pass_hash,is_admin,created_at,last_seen)"
|
||||
" VALUES (?,?,0,?,?)",
|
||||
(name, h, now, now),
|
||||
)
|
||||
return uid
|
||||
|
||||
|
||||
def touch(con, uid):
|
||||
ex(con, "UPDATE users SET last_seen=? WHERE id=?", (int(time.time()), uid))
|
||||
|
||||
|
||||
def unread(con, uid):
|
||||
return q1(con, "SELECT COUNT(*) c FROM messages WHERE to_id=? AND is_read=0",
|
||||
(uid,))["c"]
|
||||
|
||||
|
||||
def ago(ts):
|
||||
d = max(0, int(time.time()) - int(ts or 0))
|
||||
if d < 60:
|
||||
return "%ds" % d
|
||||
if d < 3600:
|
||||
return "%dm" % (d // 60)
|
||||
if d < 86400:
|
||||
return "%dh" % (d // 3600)
|
||||
return "%dd" % (d // 86400)
|
||||
Reference in New Issue
Block a user