mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 10:10:43 +00:00
手表端 - 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源) - 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、 剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等 - 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘 - 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮 滚轮点按=中键、上下滑=滚动 - 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、 点画面播放暂停、上一/下一、播完自动下一首 电脑端 - 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身 - 新增 media_list / media_pull(播放器数据源) 安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过 中转站 - known_devices 从 ID 集合改成「设备ID -> 配对码」映射 - 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉 - 老格式 state 文件平滑升级
153 lines
5.5 KiB
Python
153 lines
5.5 KiB
Python
# -*- 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())
|