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

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

中转站
- known_devices 从 ID 集合改成「设备ID -> 配对码」映射
- 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉
- 老格式 state 文件平滑升级
2026-10-18 11:39:14 +08:00

153 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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())