NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 02:02:46 +00:00
commit 40318aafdd
227 files changed
+80798

No files matched your search

+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""Patch: guest/user/owner plans with server-side session limits,
owner admin access, keyless host registration support, host/user
management endpoints, public host download."""
import re
p = "server/netdesk_server.py"
s = open(p, encoding="utf-8").read()
# ---------- 1. plans + owner ----------
old = "TUNNEL_TID = [1]"
new = """TUNNEL_TID = [1]
# session plans: guests 20 min, verified users 45 min, owner unlimited
PLAN_LIMITS = {"owner": None, "user": 45 * 60, "guest": 20 * 60}
def plan_for(email):
if email and ARGS.owner_email and email.lower() == ARGS.owner_email.lower():
return "owner", None
if email:
return "user", PLAN_LIMITS["user"]
return "guest", PLAN_LIMITS["guest"]"""
assert old in s, "p1"
s = s.replace(old, new, 1)
# ---------- 2. owner + plan on session.begin; drop hard signin gate ----------
old = """ elif t == "session.begin":
if ARGS.mail_from and not user_from_cookie(req):
log_event("auth", "connect blocked: viewer not signed in")
await send_json(ws, {"t": "session.deny", "sid": None, "reason": "signin"})
continue
hid = str(m.get("id", "")).strip()
h = HOSTS.get(hid)"""
new = """ elif t == "session.begin":
vuser = user_from_cookie(req)
vplan, vlimit = plan_for(vuser)
hid = str(m.get("id", "")).strip()
h = HOSTS.get(hid)"""
assert old in s, "p2"
s = s.replace(old, new, 1)
# after sess created, attach plan info
old = """ sess = Session(sid, h, ws)
h.session = sess
viewer_sid = sid
STATS.sessions_total += 1
log_event("session", f"viewer {peer} connecting to host {hid}")
log.info("session %s: viewer %s -> host %s", sid, peer, hid)
await send_json(h.ws, {"t": "session.begin", "sid": sid})"""
new = """ sess = Session(sid, h, ws)
sess.plan, sess.vlimit, sess.vemail = vplan, vlimit, vuser
h.session = sess
viewer_sid = sid
STATS.sessions_total += 1
log_event("session", f"viewer {peer} ({vplan}) connecting to host {hid}")
log.info("session %s: viewer %s (%s) -> host %s", sid, peer, vplan, hid)
await send_json(h.ws, {"t": "session.begin", "sid": sid})"""
assert old in s, "p3"
s = s.replace(old, new, 1)
# on accept: tell viewer the plan + start the enforcement timer
old = """ if t == "session.accept" and host is not None:
sess.active = True
log_event("session", f"session accepted on host {sess.host.id}")
await send_json(sess.viewer_ws, m)"""
new = """ if t == "session.accept" and host is not None:
sess.active = True
log_event("session", f"session accepted on host {sess.host.id}")
plan_msg = {"t": "session.plan", "plan": sess.plan,
"limit": sess.vlimit or 0, "email": sess.vemail or ""}
await send_json(sess.viewer_ws, plan_msg)
if sess.vlimit:
async def _limit_killer(s=sess):
await asyncio.sleep(s.vlimit)
if s.active:
log_event("session", f"time limit on host {s.host.id}")
await relay_session_end(
s, "session time limit reached - reconnect or sign up",
None)
if s.host:
s.host.session = None
asyncio.ensure_future(_limit_killer(sess))
await send_json(sess.viewer_ws, m)"""
assert old in s, "p4"
s = s.replace(old, new, 1)
# ---------- 3. Session fields ----------
old = """class Session:
def __init__(self, sid, host, viewer_ws):
self.sid = sid
self.host = host
self.viewer_ws = viewer_ws
self.active = False"""
new = """class Session:
def __init__(self, sid, host, viewer_ws):
self.sid = sid
self.host = host
self.viewer_ws = viewer_ws
self.active = False
self.plan = "guest"
self.vlimit = None
self.vemail = ""
self.started = time.time()"""
assert old in s, "p5"
s = s.replace(old, new, 1)
# ---------- 4. owner-aware portal auth + admin endpoints ----------
old = """def portal_authorized(req) -> bool:
if not ARGS.portal_password:
return False
tok = req.cookies.get("ndportal", "")
return tok in PORTAL_SESSIONS"""
new = """def owner_from_cookie(req) -> bool:
email = user_from_cookie(req)
return bool(email and ARGS.owner_email and email.lower() == ARGS.owner_email.lower())
def portal_authorized(req) -> bool:
if owner_from_cookie(req):
return True
if not ARGS.portal_password:
return False
tok = req.cookies.get("ndportal", "")
return tok in PORTAL_SESSIONS"""
assert old in s, "p6"
s = s.replace(old, new, 1)
# admin endpoints
old = """async def portal_download(req):"""
new = """async def portal_users(req):
if not portal_authorized(req):
return web.json_response({"error": "unauthorized"}, status=401)
users = []
with _acct_db() as conn:
for row in conn.execute("SELECT email, verified, created FROM users "
"ORDER BY created DESC LIMIT 500"):
users.append({"email": row[0], "verified": bool(row[1]), "created": row[2]})
return web.json_response({"users": users})
async def portal_user_delete(req):
if not portal_authorized(req):
return web.json_response({"error": "unauthorized"}, status=401)
body = await req.json()
email = str(body.get("email", "")).lower()
if ARGS.owner_email and email == ARGS.owner_email.lower():
return web.json_response({"error": "cannot delete the owner"}, status=400)
with _acct_db() as conn:
conn.execute("DELETE FROM users WHERE email=?", (email,))
for tok, em in list(USER_SESSIONS.items()):
if em == email:
USER_SESSIONS.pop(tok, None)
log_event("admin", f"user deleted: {email}")
return web.json_response({"ok": True})
async def portal_host_delete(req):
if not portal_authorized(req):
return web.json_response({"error": "unauthorized"}, status=401)
hid = str((await req.json()).get("id", ""))
h = HOSTS.get(hid)
if h is None:
return web.json_response({"error": "host offline (removed from registry)"}, status=404)
try:
await h.ws.close()
except Exception:
pass
log_event("admin", f"host {hid} removed by admin")
return web.json_response({"ok": True, "msg": "host removed; it must re-register"})
async def portal_session_disconnect(req):
if not portal_authorized(req):
return web.json_response({"error": "unauthorized"}, status=401)
hid = str((await req.json()).get("id", ""))
h = HOSTS.get(hid)
if h is None or not h.session:
return web.json_response({"error": "no active session"}, status=404)
await relay_session_end(h.session, "disconnected by admin", None)
h.session = None
log_event("admin", f"session on {hid} disconnected by admin")
return web.json_response({"ok": True})
async def portal_download(req):"""
assert old in s, "p7"
s = s.replace(old, new, 1)
# overview: users + richer host info
old = """ return web.json_response({
"version": VERSION,
"reg_key": ARGS.reg_key or "",
"uptime": int(now - STATS.started),"""
new = """ users = []
with _acct_db() as conn:
for row in conn.execute("SELECT email, verified, created FROM users "
"ORDER BY created DESC LIMIT 500"):
users.append({"email": row[0], "verified": bool(row[1]), "created": row[2]})
for k in known:
h = HOSTS.get(k["id"])
k["plan"] = h.session.plan if (h and h.session) else ""
return web.json_response({
"version": VERSION,
"owner": bool(ARGS.owner_email and user_from_cookie(req) and
user_from_cookie(req).lower() == ARGS.owner_email.lower()),
"users": users,
"reg_key": ARGS.reg_key or "",
"uptime": int(now - STATS.started),"""
assert old in s, "p8"
s = s.replace(old, new, 1)
# routes
old = """ app.router.add_get("/portal/api/overview", portal_overview)"""
new = """ app.router.add_get("/portal/api/overview", portal_overview)
app.router.add_get("/portal/api/users", portal_users)
app.router.add_post("/portal/api/user/delete", portal_user_delete)
app.router.add_post("/portal/api/host/delete", portal_host_delete)
app.router.add_post("/portal/api/session/disconnect", portal_session_disconnect)"""
assert old in s, "p9"
s = s.replace(old, new, 1)
# public host download (homepage button)
old = """async def tunnel_ws(req):"""
new = """async def public_download(req):
name = os.path.basename(req.match_info.get("name", ""))
path = os.path.join(FILES_DIR, name)
if not os.path.isfile(path):
return web.Response(status=404, text="not found")
with open(path, "rb") as f:
body = f.read()
resp = web.Response(body=body, content_type="application/octet-stream")
resp.headers["Content-Disposition"] = f'attachment; filename="{name}"'
return resp
async def tunnel_ws(req):"""
assert old in s, "p10"
s = s.replace(old, new, 1)
old = """ app.router.add_get("/tunnel/{hid}", tunnel_ws)"""
new = """ app.router.add_get("/tunnel/{hid}", tunnel_ws)
app.router.add_get("/download/{name}", public_download)"""
assert old in s; s = s.replace(old, new, 1)
# ---------- 5. CLI owner arg ----------
old = """ ap.add_argument("--mail-from", dest="mail_from", default="","""
new = """ ap.add_argument("--owner-email", dest="owner_email", default="",
help="email with unrestricted access + admin panel")
ap.add_argument("--mail-from", dest="mail_from", default="","""
assert old in s; s = s.replace(old, new, 1)
open(p, "w", encoding="utf-8").write(s)
print("plans/owner/admin patch applied")