# -*- coding: utf-8 -*- r'''Halo 发文章(最终版):RSA 加密登录 + 建文章。 == 前面六次都失败,原因是这个 == Halo 的登录页用 **jsencrypt** 把密码 **RSA 加密**后才提交:
<- JS 把密文填这里 <- 用户输入的明文
**所以在终端直接发明文密码永远登不上** —— 服务端拿到的是密文, 它会 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())