#!/usr/bin/python3 """Authenticated local task broker. Only the web service UID may connect to its socket.""" import argparse import getpass import hashlib import hmac import json import os from pathlib import Path try:import pwd except ImportError:pwd=None import secrets import socket import socketserver import struct import re import datetime as dt import threading import time import task_core as core ADMIN=Path('/etc/hardware-task-admin.json') MONITOR_CONFIG=Path('/etc/hardware-watch.json') SOCKET=Path('/run/hardware-taskd/control.sock') SESSIONS={};ATTEMPTS={};GUARD=threading.RLock();MUTATE=threading.RLock() ALLOWED_UID=None def password_record(password): salt=secrets.token_hex(16) return {'username':'admin','salt':salt,'hash':hashlib.pbkdf2_hmac('sha256',password.encode(),bytes.fromhex(salt),240000).hex()} def verify(password): try: record=json.loads(ADMIN.read_text()) hashed=hashlib.pbkdf2_hmac('sha256',password.encode(),bytes.fromhex(record['salt']),240000).hex() return hmac.compare_digest(record['hash'],hashed) except (OSError,ValueError,KeyError):return False def create_admin(): if ADMIN.exists():return password=secrets.token_urlsafe(18) core.atom_write(ADMIN,json.dumps(password_record(password))) core.atom_write(Path('/root/hardware-task-admin-login.txt'), '定时任务管理员登录信息\n\n网页:http://192.168.1.37:8090/#tasks\n账号:admin\n初始密码:'+password+ '\n\n此账号可以管理以 root 身份执行的服务器任务,请妥善保存。登录后可修改管理密码。\nSSH root 密码未被修改,也没有作为网页管理密码保存。\n') print('初始登录信息已保存到 /root/hardware-task-admin-login.txt(仅 root 可读)。') def session(token,csrf=None,write=False): with GUARD: record=SESSIONS.get(token) if not record or record['expires']=6:raise core.TaskError('登录失败次数较多,请五分钟后重试。',429) if payload.get('username')!='admin' or not verify(str(payload.get('password',''))): ATTEMPTS[client]=recent+[now] core.audit('login_failed',client=client) raise core.TaskError('账号或密码不正确。',401) ATTEMPTS.pop(client,None) if len(SESSIONS)>200: for key in list(SESSIONS): if SESSIONS[key]['expires']131072:raise core.TaskError('请求内容过大。',413) request=json.loads(raw) response={'ok':True,'data':dispatch(request)} except core.TaskError as exc:response={'ok':False,'error':str(exc),'status':exc.status} except Exception as exc: print('task broker error:',type(exc).__name__,str(exc),flush=True) response={'ok':False,'error':'任务操作未完成,请查看任务管理服务日志。','status':500} try:self.wfile.write(json.dumps(response,ensure_ascii=False).encode()+b'\n') except (BrokenPipeError,ConnectionResetError):pass class Server(getattr(socketserver,'ThreadingUnixStreamServer',socketserver.ThreadingTCPServer)): daemon_threads=True def maintenance(): while True: try: with core.db() as con: rows=con.execute("SELECT * FROM runs WHERE state='running'").fetchall() boot=Path('/proc/sys/kernel/random/boot_id').read_text().strip() for row in rows: if row['boot']!=boot or (time.time()-row['started']>30 and not core.running(row['task_id'])): with core.db() as con: con.execute("UPDATE runs SET state='interrupted',finished=? WHERE id=? AND state='running'",(time.time(),row['id'])) with (core.LOGS/(row['id']+'.log')).open('ab') as f: f.write(('\n[执行被系统重启或进程中断,没有取得完整退出结果。]\n').encode()) cutoff=time.time()-90*86400 with core.db() as con: old=con.execute('SELECT id FROM runs WHERE started