"""bluauto webhook receiver. Put behind an HTTPS reverse proxy; Python 3.10+.""" import os,json,hmac,hashlib,time,sqlite3,re from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer def database(path): db=sqlite3.connect(path,timeout=10) db.execute('PRAGMA journal_mode=WAL') db.executescript('CREATE TABLE IF NOT EXISTS orders(order_id TEXT PRIMARY KEY,invoice_id TEXT UNIQUE NOT NULL,amount_rial TEXT NOT NULL,paid INTEGER NOT NULL DEFAULT 0);CREATE TABLE IF NOT EXISTS webhook_receipts(event_id TEXT PRIMARY KEY,invoice_id TEXT NOT NULL); CREATE TABLE IF NOT EXISTS fulfillment_jobs(order_id TEXT PRIMARY KEY,status TEXT NOT NULL DEFAULT 0);') return db def receive(raw,headers,path,secret,now=None): now=int(time.time()) if now is None else now ts=headers.get('X-Didban-Timestamp','');sig=headers.get('X-Didban-Signature','') if not secret: return 503,'Webhook secret is not configured' if not re.fullmatch(r'\d{1,12}',ts) or abs(now-int(ts))>300: return 401,'Expired signature' expected='v1='+hmac.new(secret.encode(),ts.encode()+b'.'+raw,hashlib.sha256).hexdigest() if not hmac.compare_digest(expected,sig): return 401,'Invalid signature' try: e=json.loads(raw);d=e['data'];event=e['event_id'] if not isinstance(event,str) or not 1<=len(event)<=100 or event!=headers.get('X-Didban-Event-Id'): return 400,'Invalid event' if e['type']=='webhook.test' and d.get('mode')=='test' and d.get('paid') is False: return 200,'Test acknowledged' if e['type']!='invoice.paid' or d['mode']!='live' or d['status']!='PAID' or d['paid'] is not True: return 400,'Unsupported event' if not isinstance(d['amount_rial'],str) or not re.fullmatch(r'[1-9]\d{0,23}',d['amount_rial']): return 400,'Invalid amount' db=database(path) try: db.execute('BEGIN IMMEDIATE') row=db.execute('SELECT invoice_id,amount_rial,paid FROM orders WHERE order_id=?',(d['order_id'],)).fetchone() if not row or row[0]!=d['id'] or row[1]!=d['amount_rial']: db.rollback();return 409,'Order mismatch' old=db.execute('SELECT invoice_id FROM webhook_receipts WHERE event_id=?',(event,)).fetchone() if old and old[0]!=d['id']: db.rollback();return 409,'Event conflict' db.execute('INSERT OR IGNORE INTO webhook_receipts VALUES(?,?)',(event,d['id'])) db.execute('UPDATE orders SET paid=1 WHERE order_id=? AND paid=0',(d['order_id'],)) db.execute('INSERT OR IGNORE INTO fulfillment_jobs(order_id) VALUES(?)',(d['order_id'],)) # Queue fulfillment inside this transaction using a unique order_id. # Do not send goods or call external services before committing. db.commit();return 200,'OK' finally: db.close() except (KeyError,ValueError,TypeError): return 400,'Invalid payload' class Handler(BaseHTTPRequestHandler): def do_POST(self): if self.path!='/webhook': self.send_error(404);return try: size=int(self.headers.get('Content-Length','0')) except ValueError: self.send_error(400);return if not 0