2.3.0:手表界面精简 + 播放器 + 鼠标重做

手表端
- 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源)
- 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、
  剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等
- 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘
- 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮
  滚轮点按=中键、上下滑=滚动
- 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、
  点画面播放暂停、上一/下一、播完自动下一首

电脑端
- 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身
- 新增 media_list / media_pull(播放器数据源)
  安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过

中转站
- known_devices 从 ID 集合改成「设备ID -> 配对码」映射
- 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉
- 老格式 state 文件平滑升级
This commit is contained in:
xmz7891
2026-10-18 11:39:14 +08:00
parent 1d368979aa
commit 2061f9525f
50 changed files with 13812 additions and 584 deletions
+152
View File
@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
r'''Halo 发文章(最终版):RSA 加密登录 + 建文章。
== 前面六次都失败,原因是这个 ==
Halo 的登录页用 **jsencrypt** 把密码 **RSA 加密**后才提交:
<script src="/webjars/jsencrypt/3.5.4/bin/jsencrypt.min.js"></script>
<form action="/login" method="post">
<input type="hidden" name="_csrf" ... />
<input type="hidden" name="password" /> <- JS 把密文填这里
<input name="username" />
<input id="plainPassword" /> <- 用户输入的明文
</form>
**所以在终端直接发明文密码永远登不上** —— 服务端拿到的是密文,
它会 RSA 解密再比对;我给明文,解密失败,于是 `invalid-credential`。
这个坑很深,因为**报错信息是"凭据无效"** —— 会让人一直怀疑
"是不是密码打错了",而不是"我流程不对"。
== 踩过的另外几个 ==
* `/login` 默认显示**邮箱验证码**表单(`action="/login/email-code"`),
密码表单在 **`/login?method=local`**。我一开始连页面都拿错了。
* CSRF 必须带(Spring Security 默认开),不带直接 403。
* 密码文件被 PowerShell 写了个 **BOM**(`\ufeff`),
读出来多一个不可见字符 —— 又一个"看着没错实际不对"的坑。
* Halo 有**登录失败限流**,试错几次就被挡住,得等。
== jsencrypt 的填充方式 ==
`jsencrypt` 默认用 **PKCS#1 v1.5** padding。加密后的密文是 base64。
公钥在登录页的 JS 里(一个大 base64 串),注意 HTML 里 `\/` 要还原成 `/`。
'''
from __future__ import annotations
import base64
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from lssh import run # noqa: E402
PWFILE = ROOT / "_build" / ".halo_password"
USER = "xmz7891"
# 在服务器上跑的 Python:读公钥 -> RSA 加密 -> 输出 base64
ENCRYPT_PY = r'''
import base64, re, sys, json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
html = open('/tmp/lg9.html', encoding='utf-8', errors='replace').read()
m = re.search(r'publicKey["\']?\s*[:=]\s*["\']([^"\']{40,})["\']', html)
if not m:
print("NO_KEY"); sys.exit(1)
b64 = m.group(1).replace('\\/', '/').replace('\\n', '')
b64 = re.sub(r'\s+', '', b64)
try:
der = base64.b64decode(b64)
pub = serialization.load_der_public_key(der)
except Exception as e1:
try:
pem = b'-----BEGIN PUBLIC KEY-----\n' + \
b'\n'.join(b64[i:i+64].encode() for i in range(0, len(b64), 64)) + \
b'\n-----END PUBLIC KEY-----\n'
pub = serialization.load_pem_public_key(pem)
except Exception as e2:
print("LOAD_FAIL: %s / %s" % (e1, e2)); sys.exit(1)
pw = open('/tmp/halo_pw.txt', 'rb').read().strip()
ct = pub.encrypt(pw, padding.PKCS1v15())
print(base64.b64encode(ct).decode())
'''
def main() -> int:
print("=" * 62)
print(" Halo:RSA 登录 -> 发文章")
print("=" * 62)
print()
pw = PWFILE.read_text(encoding="utf-8").strip()
print(" 用户名: %s" % USER)
print(" 密码长度: %d" % len(pw))
# 把加密脚本和密码传上去
enc = ROOT / "_build" / "_halo_encrypt.py"
enc.write_text(ENCRYPT_PY, encoding="utf-8", newline="\n")
from lssh import upload
upload(str(enc), "/tmp/halo_encrypt.py")
enc.unlink(missing_ok=True)
out = run(r'''
set -e
# 密码写成无 BOM 的文件
printf '%s' '__PW__' > /tmp/halo_pw.txt
echo " 密码文件字节数: $(wc -c < /tmp/halo_pw.txt)"
# 拿登录页(含公钥)
curl -s --max-time 12 "http://127.0.0.1:3001/login?method=local" -o /tmp/lg9.html
echo " 登录页 $(wc -c < /tmp/lg9.html) 字节"
# RSA 加密
CT=$(python3 /tmp/halo_encrypt.py)
if [ -z "$CT" ] || echo "$CT" | grep -qE '^(NO_KEY|LOAD_FAIL)'; then
echo " ✗ 加密失败: $CT"; exit 1
fi
echo " ✓ 密文长度 ${#CT}"
printf '%s' "$CT" > /tmp/halo_ct.txt
# 登录
rm -f /tmp/hc9.txt
curl -s -c /tmp/hc9.txt -o /tmp/lg9b.html --max-time 12 "http://127.0.0.1:3001/login?method=local"
CSRF=$(grep -oE 'name="_csrf" value="[^"]+"' /tmp/lg9b.html | head -1 | sed 's/.*value="//; s/"//')
echo " csrf 长度 ${#CSRF}"
LOC=$(curl -s -b /tmp/hc9.txt -c /tmp/hc9.txt -o /dev/null -w '%{redirect_url}' \
--max-time 20 -X POST "http://127.0.0.1:3001/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "_csrf=$CSRF" \
--data-urlencode "username=__U__" \
--data-urlencode "password=$CT")
echo " 登录跳转: ${LOC:-(空=可能成功)}"
CODE=$(curl -s -b /tmp/hc9.txt -o /dev/null -w '%{http_code}' --max-time 12 \
"http://127.0.0.1:3001/apis/api.console.halo.run/v1alpha1/posts?page=0&size=3")
echo " 带 cookie 打 API: HTTP $CODE"
if [ "$CODE" = "200" ]; then
cp /tmp/hc9.txt /tmp/hc_good.txt
echo " ✅ 登录成功"
echo
echo " --- 现在的文章 ---"
curl -s -b /tmp/hc9.txt --max-time 12 \
"http://127.0.0.1:3001/apis/api.console.halo.run/v1alpha1/posts?page=0&size=20" \
| python3 -c "
import json,sys
d=json.load(sys.stdin)
print(' 共', d.get('total'), '篇')
for p in (d.get('items') or []):
s=p.get('spec',{}); print(' -', repr(s.get('title')), '| publish=', s.get('publish'))
"
fi
'''.replace("__PW__", pw).replace("__U__", USER), timeout=600, quiet=True)
print(out[1] if isinstance(out, tuple) else out)
return 0
if __name__ == "__main__":
sys.exit(main())