Archived
140 lines
5.7 KiB
Python
140 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import secrets
|
|
import sqlite3
|
|
from datetime import UTC, datetime
|
|
|
|
from ..ddns_security import hash_lookup_token
|
|
from .types import DomainChallengeRow
|
|
|
|
|
|
class DomainsMixin:
|
|
def create_domain_challenge(self, domain: str, owner_hostname: str | None = None) -> DomainChallengeRow:
|
|
token = f"ff-ddns-{secrets.token_urlsafe(24)}"
|
|
claim_secret = secrets.token_urlsafe(32)
|
|
claim_hash = hash_lookup_token(claim_secret)
|
|
now = datetime.now(UTC).isoformat()
|
|
with self.connect() as conn:
|
|
existing = conn.execute(
|
|
"SELECT verified_at, owner_hostname FROM domain_challenges WHERE domain = ?",
|
|
(domain,),
|
|
).fetchone()
|
|
if existing and existing[0]:
|
|
raise sqlite3.IntegrityError("domain is already verified")
|
|
if existing and existing[1] and existing[1] != owner_hostname:
|
|
raise sqlite3.IntegrityError("domain claim belongs to another dashboard")
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO domain_challenges(domain, token, claim_hash, owner_hostname, created_at, verified_at)
|
|
VALUES (?, ?, ?, ?, ?, NULL)
|
|
ON CONFLICT(domain) DO UPDATE SET
|
|
token=excluded.token,
|
|
claim_hash=excluded.claim_hash,
|
|
owner_hostname=excluded.owner_hostname,
|
|
created_at=excluded.created_at,
|
|
verified_at=NULL
|
|
""",
|
|
(domain, token, claim_hash, owner_hostname, now),
|
|
)
|
|
return {"domain": domain, "token": token, "claim_secret": claim_secret}
|
|
|
|
def verify_domain_challenge(
|
|
self,
|
|
domain: str,
|
|
claim_secret: str,
|
|
owner_hostname: str | None = None,
|
|
) -> DomainChallengeRow | None:
|
|
now = datetime.now(UTC).isoformat()
|
|
claim_hash = hash_lookup_token(claim_secret)
|
|
with self.connect() as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
sql = "SELECT domain, token, owner_hostname, verified_at FROM domain_challenges WHERE domain = ? AND claim_hash = ?"
|
|
params = [domain, claim_hash]
|
|
if owner_hostname is not None:
|
|
sql += " AND owner_hostname = ?"
|
|
params.append(owner_hostname)
|
|
challenge = conn.execute(sql, tuple(params)).fetchone()
|
|
if not challenge:
|
|
return None
|
|
update_sql = "UPDATE domain_challenges SET verified_at = ? WHERE domain = ? AND claim_hash = ?"
|
|
update_params = [now, domain, claim_hash]
|
|
if owner_hostname is not None:
|
|
update_sql += " AND owner_hostname = ?"
|
|
update_params.append(owner_hostname)
|
|
cursor = conn.execute(update_sql, tuple(update_params))
|
|
if cursor.rowcount != 1:
|
|
return None
|
|
result = dict(challenge)
|
|
result["verified_at"] = now
|
|
result["claim_secret"] = claim_secret
|
|
return result
|
|
|
|
def get_domain_challenge(
|
|
self,
|
|
domain: str,
|
|
claim_secret: str | None = None,
|
|
owner_hostname: str | None = None,
|
|
) -> DomainChallengeRow | None:
|
|
with self.connect() as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
sql = "SELECT domain, token, owner_hostname, verified_at FROM domain_challenges WHERE domain = ?"
|
|
params = [domain]
|
|
if claim_secret is not None:
|
|
sql += " AND claim_hash = ?"
|
|
params.append(hash_lookup_token(claim_secret))
|
|
if owner_hostname is not None:
|
|
sql += " AND owner_hostname = ?"
|
|
params.append(owner_hostname)
|
|
challenge = conn.execute(sql, tuple(params)).fetchone()
|
|
return dict(challenge) if challenge else None
|
|
|
|
def get_domain_challenge_by_secret(self, claim_secret: str) -> DomainChallengeRow | None:
|
|
with self.connect() as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
challenge = conn.execute(
|
|
"SELECT domain, token, owner_hostname, verified_at FROM domain_challenges WHERE claim_hash = ?",
|
|
(hash_lookup_token(claim_secret),),
|
|
).fetchone()
|
|
if not challenge:
|
|
return None
|
|
result = dict(challenge)
|
|
result["claim_secret"] = claim_secret
|
|
return result
|
|
|
|
def list_domain_challenges(self, limit: int = 100, offset: int = 0) -> list[DomainChallengeRow]:
|
|
with self.connect() as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT domain, owner_hostname, created_at, verified_at
|
|
FROM domain_challenges
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?
|
|
""",
|
|
(limit, offset),
|
|
).fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
def count_domain_challenges(self) -> int:
|
|
with self.connect() as conn:
|
|
row = conn.execute("SELECT COUNT(*) FROM domain_challenges").fetchone()
|
|
return int(row[0])
|
|
|
|
def is_domain_verified(
|
|
self,
|
|
domain: str,
|
|
claim_secret: str | None = None,
|
|
owner_hostname: str | None = None,
|
|
) -> bool:
|
|
with self.connect() as conn:
|
|
sql = "SELECT verified_at FROM domain_challenges WHERE domain = ?"
|
|
params = [domain]
|
|
if claim_secret is not None:
|
|
sql += " AND claim_hash = ?"
|
|
params.append(hash_lookup_token(claim_secret))
|
|
if owner_hostname is not None:
|
|
sql += " AND owner_hostname = ?"
|
|
params.append(owner_hostname)
|
|
verified_at = conn.execute(sql, tuple(params)).fetchone()
|
|
return bool(verified_at and verified_at[0])
|