Commit 4b6e1572ef46120a307a55ef898974747a732289

Authored by yibo
1 parent cfac4e65

feat(xhshook): v3.1 签名代理模式 — OkHttp proxy poller + body 捕获

Hook.java v3.1 (315 行):
- body 捕获:okio.Buffer 读取 POST/PUT body(截断 2000 字符)
- Proxy poller(后台线程,1s 轮询 /xhs_proxy/pending):
    executeOkHttp() 在 XHS 进程内用 OkHttpClient 执行代理请求,
    自动经过 XHS 拦截器签名,结果 POST /xhs_proxy/result
- 手写 JSON 解析器(无外部依赖)
- 保留 /xhs_params 转发(每条 edith 请求实时上报)

event_server.py(Hermes 编写):
- proxy_requests 表(pending→ok/error 状态机)
- POST /xhs_proxy — Hermes 下发代理请求
- GET  /xhs_proxy/pending — Hook 轮询取下一个任务
- POST /xhs_proxy/result — Hook 上报执行结果
- GET  /xhs_proxy/result/{id} — Hermes 轮询取结果
- DELETE /xhs_proxy/{id} — 清理完成的任务

架构:Hermes → event_server → Hook(XHS进程内OkHttp签名) → event_server → Hermes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
phoneuse/event_server.py
... ... @@ -219,7 +219,7 @@ class XhsParams(BaseModel):
219 219 method: str
220 220 url: str
221 221 headers: dict[str, str] = {}
222   -
  222 + body: str = ""
223 223  
224 224 @app.post("/xhs_params", status_code=200)
225 225 def post_xhs_params(params: XhsParams):
... ... @@ -228,7 +228,10 @@ def post_xhs_params(params: XhsParams):
228 228 with get_db() as conn:
229 229 cursor = conn.execute(
230 230 "INSERT INTO events (type, app, data, ts, source) VALUES (?, ?, ?, ?, ?)",
231   - ("xhs_request", "com.xingin.xhs", json.dumps({"method": params.method, "url": params.url, "headers": params.headers}), time.time(), "xhshook")
  231 + ("xhs_request", "com.xingin.xhs",
  232 + json.dumps({"method": params.method, "url": params.url,
  233 + "headers": params.headers, "body": params.body}),
  234 + time.time(), "xhshook")
232 235 )
233 236 conn.commit()
234 237 return {"id": cursor.lastrowid}
... ... @@ -243,3 +246,102 @@ def get_xhs_params(limit: int = Query(default=10, le=100)):
243 246 (limit,)
244 247 ).fetchall()
245 248 return [{"id": r["id"], "ts": r["ts"], **json.loads(r["data"])} for r in reversed(rows)]
  249 +
  250 +
  251 +# ─── XHS Proxy (Hermes → xhshook → OkHttp) ───
  252 +
  253 +class XhsProxyRequest(BaseModel):
  254 + url: str
  255 + method: str = "GET"
  256 + body: str = ""
  257 + headers_extra: dict[str, str] = {}
  258 +
  259 +class XhsProxyResult(BaseModel):
  260 + proxy_id: int
  261 + status: str # "ok" | "error"
  262 + http_code: int = 0
  263 + response_body: str = ""
  264 + error: str = ""
  265 +
  266 +def _init_proxy_table():
  267 + with get_db() as conn:
  268 + conn.execute("""
  269 + CREATE TABLE IF NOT EXISTS proxy_requests (
  270 + id INTEGER PRIMARY KEY AUTOINCREMENT,
  271 + status TEXT DEFAULT 'pending',
  272 + created_ts REAL,
  273 + req_url TEXT,
  274 + req_method TEXT,
  275 + req_body TEXT,
  276 + req_headers_extra TEXT,
  277 + resp_http_code INTEGER,
  278 + resp_body TEXT,
  279 + resp_error TEXT
  280 + )
  281 + """)
  282 + conn.commit()
  283 +
  284 +_init_proxy_table()
  285 +
  286 +@app.post("/xhs_proxy", status_code=201)
  287 +def submit_proxy_request(req: XhsProxyRequest):
  288 + """Hermes submits a request to be proxied through XHS's OkHttp (with auto-signing)."""
  289 + with get_db() as conn:
  290 + cur = conn.execute(
  291 + "INSERT INTO proxy_requests (status, created_ts, req_url, req_method, req_body, req_headers_extra) "
  292 + "VALUES ('pending', ?, ?, ?, ?, ?)",
  293 + (time.time(), req.url, req.method, req.body, json.dumps(req.headers_extra))
  294 + )
  295 + conn.commit()
  296 + return {"proxy_id": cur.lastrowid}
  297 +
  298 +@app.get("/xhs_proxy/pending")
  299 +def get_pending_proxy():
  300 + """xhshook polls this. Returns oldest pending request (null JSON if none)."""
  301 + with get_db() as conn:
  302 + row = conn.execute(
  303 + "SELECT id, req_url, req_method, req_body, req_headers_extra FROM proxy_requests "
  304 + "WHERE status='pending' ORDER BY id ASC LIMIT 1"
  305 + ).fetchone()
  306 + if not row:
  307 + return None
  308 + # Mark as processing
  309 + with get_db() as conn:
  310 + conn.execute("UPDATE proxy_requests SET status='processing' WHERE id=?", (row["id"],))
  311 + conn.commit()
  312 + return {
  313 + "proxy_id": row["id"],
  314 + "url": row["req_url"],
  315 + "method": row["req_method"],
  316 + "body": row["req_body"],
  317 + "headers_extra": json.loads(row["req_headers_extra"] or "{}")
  318 + }
  319 +
  320 +@app.post("/xhs_proxy/result", status_code=200)
  321 +def submit_proxy_result(result: XhsProxyResult):
  322 + """xhshook posts the API response here."""
  323 + with get_db() as conn:
  324 + conn.execute(
  325 + "UPDATE proxy_requests SET status=?, resp_http_code=?, resp_body=?, resp_error=? WHERE id=?",
  326 + (result.status, result.http_code, result.response_body, result.error, result.proxy_id)
  327 + )
  328 + conn.commit()
  329 + return {"ok": True}
  330 +
  331 +@app.get("/xhs_proxy/result/{proxy_id}")
  332 +def get_proxy_result(proxy_id: int):
  333 + """Hermes polls this for the result."""
  334 + with get_db() as conn:
  335 + row = conn.execute(
  336 + "SELECT * FROM proxy_requests WHERE id=?", (proxy_id,)
  337 + ).fetchone()
  338 + if not row:
  339 + raise HTTPException(404, "proxy_id not found")
  340 + return {
  341 + "proxy_id": row["id"],
  342 + "status": row["status"],
  343 + "created_ts": row["created_ts"],
  344 + "http_code": row["resp_http_code"],
  345 + "response_body": row["resp_body"],
  346 + "error": row["resp_error"]
  347 + }
... ...
xhs_lsposed_a16_fix/xhshook_src/Hook.java
... ... @@ -6,20 +6,28 @@ import de.robv.android.xposed.XposedBridge;
6 6 import de.robv.android.xposed.XposedHelpers;
7 7 import de.robv.android.xposed.callbacks.XC_LoadPackage;
8 8  
  9 +import java.io.InputStream;
9 10 import java.io.OutputStream;
  11 +import java.io.ByteArrayOutputStream;
10 12 import java.net.HttpURLConnection;
11 13 import java.net.URL;
12 14 import java.nio.charset.StandardCharsets;
  15 +import java.util.Scanner;
13 16  
14 17 public class Hook implements IXposedHookLoadPackage {
15   - // Hermes event_server endpoint — POST xhs_params events here
  18 + // Hermes event_server endpoints
16 19 private static final String HERMES_URL = "http://127.0.0.1:8765/xhs_params";
  20 + private static final String PROXY_POLL_URL = "http://127.0.0.1:8765/xhs_proxy/pending";
  21 + private static final String PROXY_RESULT_URL = "http://127.0.0.1:8765/xhs_proxy/result";
  22 + private static ClassLoader appClassLoader;
  23 + private static boolean proxyRunning = false;
17 24  
18 25 @Override
19 26 public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
20 27 if (!lpparam.packageName.equals("com.xingin.xhs")) return;
21 28  
22 29 XposedBridge.log("[XHSHook] Loaded into XHS!");
  30 + appClassLoader = lpparam.classLoader;
23 31  
24 32 try {
25 33 Class<?> clz = XposedHelpers.findClass("okhttp3.Request$Builder", lpparam.classLoader);
... ... @@ -42,13 +50,25 @@ public class Hook implements IXposedHookLoadPackage {
42 50 headersStr = headers != null ? headers.toString() : "";
43 51 }
44 52  
45   - XposedBridge.log("[XHSHook] " + methodStr + " " + urlStr);
46   - XposedBridge.log("[XHSHook] Headers:\n" + headersStr);
  53 + // Capture request body (for POST/PUT requests)
  54 + String bodyStr = "";
  55 + try {
  56 + Object bodyObj = XposedHelpers.getObjectField(param.thisObject, "body");
  57 + if (bodyObj != null) {
  58 + Class<?> bufferClz = XposedHelpers.findClass("okio.Buffer", lpparam.classLoader);
  59 + Object buffer = bufferClz.newInstance();
  60 + XposedHelpers.callMethod(bodyObj, "writeTo", buffer);
  61 + bodyStr = (String) XposedHelpers.callMethod(buffer, "readUtf8");
  62 + if (bodyStr != null && bodyStr.length() > 2000) {
  63 + bodyStr = bodyStr.substring(0, 2000);
  64 + }
  65 + }
  66 + } catch (Throwable t) {
  67 + bodyStr = "";
  68 + }
47 69  
48   - final String json = buildJson(methodStr, urlStr, headersStr);
49   - new Thread(new Runnable() {
50   - public void run() { postToHermes(json); }
51   - }).start();
  70 + final String json = buildJson(methodStr, urlStr, headersStr, bodyStr);
  71 + new Thread(() -> postToHermes(json)).start();
52 72  
53 73 } catch (Throwable t) {
54 74 XposedBridge.log("[XHSHook] Error in hook: " + t.getMessage());
... ... @@ -56,17 +76,201 @@ public class Hook implements IXposedHookLoadPackage {
56 76 }
57 77 });
58 78 XposedBridge.log("[XHSHook] OkHttp Request.Builder.build() hooked!");
  79 +
  80 + // Start proxy poller in background
  81 + startProxyPoller();
  82 +
59 83 } catch (Throwable t) {
60 84 XposedBridge.log("[XHSHook] Failed to hook OkHttp: " + t.getMessage());
61 85 XposedBridge.log(t);
62 86 }
63 87 }
64 88  
65   - private static String buildJson(String method, String url, String headersStr) {
  89 + // ─── Proxy Poller ─────────────────────────────────────────────
  90 +
  91 + private static void startProxyPoller() {
  92 + if (proxyRunning) return;
  93 + proxyRunning = true;
  94 + new Thread(() -> {
  95 + XposedBridge.log("[XHSHook] Proxy poller started");
  96 + while (true) {
  97 + try {
  98 + String json = httpGet(PROXY_POLL_URL);
  99 + if (json != null && !json.equals("null") && !json.isEmpty()) {
  100 + int proxyId = parseJsonInt(json, "proxy_id");
  101 + String reqUrl = parseJsonString(json, "url");
  102 + String method = parseJsonString(json, "method");
  103 + String body = parseJsonString(json, "body");
  104 +
  105 + XposedBridge.log("[XHSHook] Proxy exec #" + proxyId + " " + method + " " + reqUrl);
  106 +
  107 + String[] result = executeOkHttp(proxyId, method, reqUrl, body);
  108 + String status = result[0];
  109 + int httpCode = Integer.parseInt(result[1]);
  110 + String respBody = result[2];
  111 + String error = result[3];
  112 +
  113 + postProxyResult(proxyId, status, httpCode, respBody, error);
  114 + }
  115 + Thread.sleep(1000);
  116 + } catch (Throwable t) {
  117 + XposedBridge.log("[XHSHook] Proxy poller error: " + t.getMessage());
  118 + try { Thread.sleep(3000); } catch (Throwable tt) {}
  119 + }
  120 + }
  121 + }).start();
  122 + }
  123 +
  124 + private static String[] executeOkHttp(int proxyId, String method, String url, String body) {
  125 + // Returns [status, httpCode, responseBody, error]
  126 + try {
  127 + Class<?> okHttpClz = XposedHelpers.findClass("okhttp3.OkHttpClient", appClassLoader);
  128 + Class<?> requestClz = XposedHelpers.findClass("okhttp3.Request", appClassLoader);
  129 + Class<?> requestBuilderClz = XposedHelpers.findClass("okhttp3.Request$Builder", appClassLoader);
  130 + Class<?> mediaTypeClz = XposedHelpers.findClass("okhttp3.MediaType", appClassLoader);
  131 + Class<?> requestBodyClz = XposedHelpers.findClass("okhttp3.RequestBody", appClassLoader);
  132 +
  133 + // Create OkHttpClient (new instance — auto-injects interceptors from XHS)
  134 + Object client = okHttpClz.newInstance();
  135 +
  136 + // Build request
  137 + Object builder = requestBuilderClz.newInstance();
  138 + builder = XposedHelpers.callMethod(builder, "url", url);
  139 + builder = XposedHelpers.callMethod(builder, "method", method,
  140 + body != null && !body.isEmpty() ?
  141 + XposedHelpers.callStaticMethod(requestBodyClz, "create",
  142 + XposedHelpers.callStaticMethod(mediaTypeClz, "parse", "application/json; charset=utf-8"),
  143 + body)
  144 + : null);
  145 +
  146 + // Add standard XHS headers (User-Agent, Referer)
  147 + builder = XposedHelpers.callMethod(builder, "addHeader", "User-Agent",
  148 + "Dalvik/2.1.0 (Linux; U; Android 16; Pixel 10 Pro Fold Build/CP1A.260505.005) " +
  149 + "Resolution/1080*2364 Version/9.35.1 Build/9351803 Device/(Google;Pixel 10 Pro Fold) " +
  150 + "discover/9.35.1 NetType/WiFi");
  151 + builder = XposedHelpers.callMethod(builder, "addHeader", "Referer", "https://app.xhs.cn/");
  152 +
  153 + Object request = XposedHelpers.callMethod(builder, "build");
  154 +
  155 + // Execute
  156 + Object call = XposedHelpers.callMethod(client, "newCall", request);
  157 + Object response = XposedHelpers.callMethod(call, "execute");
  158 +
  159 + int httpCode = (Integer) XposedHelpers.callMethod(response, "code");
  160 + Object respBodyObj = XposedHelpers.callMethod(response, "body");
  161 + Object source = XposedHelpers.callMethod(respBodyObj, "source");
  162 + Object buffer = XposedHelpers.findClass("okio.Buffer", appClassLoader).newInstance();
  163 + XposedHelpers.callMethod(source, "readAll", buffer);
  164 + String respBody = (String) XposedHelpers.callMethod(buffer, "readUtf8");
  165 +
  166 + // Truncate
  167 + if (respBody != null && respBody.length() > 10000) {
  168 + respBody = respBody.substring(0, 10000);
  169 + }
  170 +
  171 + XposedHelpers.callMethod(response, "close");
  172 + return new String[]{"ok", String.valueOf(httpCode), respBody, ""};
  173 +
  174 + } catch (Throwable t) {
  175 + String errMsg = t.getMessage();
  176 + if (errMsg == null) errMsg = t.getClass().getName();
  177 + return new String[]{"error", "0", "", errMsg};
  178 + }
  179 + }
  180 +
  181 + private static void postProxyResult(int proxyId, String status, int httpCode, String respBody, String error) {
  182 + String json = "{\"proxy_id\":" + proxyId +
  183 + ",\"status\":\"" + status + "\"" +
  184 + ",\"http_code\":" + httpCode +
  185 + ",\"response_body\":" + jsonStr(respBody) +
  186 + ",\"error\":" + jsonStr(error) + "}";
  187 + try {
  188 + HttpURLConnection conn = (HttpURLConnection) new URL(PROXY_RESULT_URL).openConnection();
  189 + conn.setRequestMethod("POST");
  190 + conn.setRequestProperty("Content-Type", "application/json");
  191 + conn.setDoOutput(true);
  192 + conn.setConnectTimeout(2000);
  193 + conn.setReadTimeout(2000);
  194 + byte[] bodyBytes = json.getBytes(StandardCharsets.UTF_8);
  195 + conn.setFixedLengthStreamingMode(bodyBytes.length);
  196 + try (OutputStream os = conn.getOutputStream()) {
  197 + os.write(bodyBytes);
  198 + }
  199 + int code = conn.getResponseCode();
  200 + conn.disconnect();
  201 + if (code != 200) {
  202 + XposedBridge.log("[XHSHook] Proxy result POST " + code);
  203 + }
  204 + } catch (Throwable t) {
  205 + XposedBridge.log("[XHSHook] Failed to post proxy result: " + t.getMessage());
  206 + }
  207 + }
  208 +
  209 + // ─── HTTP helpers ─────────────────────────────────────────────
  210 +
  211 + private static String httpGet(String urlStr) {
  212 + try {
  213 + HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
  214 + conn.setRequestMethod("GET");
  215 + conn.setConnectTimeout(2000);
  216 + conn.setReadTimeout(2000);
  217 + int code = conn.getResponseCode();
  218 + if (code == 200) {
  219 + Scanner s = new Scanner(conn.getInputStream(), "UTF-8").useDelimiter("\\A");
  220 + String result = s.hasNext() ? s.next() : "";
  221 + s.close();
  222 + conn.disconnect();
  223 + return result;
  224 + }
  225 + conn.disconnect();
  226 + } catch (Throwable t) {}
  227 + return null;
  228 + }
  229 +
  230 + // ─── Minimal JSON parsing (no external deps) ──────────────────
  231 +
  232 + private static int parseJsonInt(String json, String key) {
  233 + int idx = json.indexOf("\"" + key + "\"");
  234 + if (idx < 0) return 0;
  235 + idx = json.indexOf(":", idx);
  236 + if (idx < 0) return 0;
  237 + idx++;
  238 + while (idx < json.length() && (json.charAt(idx) == ' ' || json.charAt(idx) == '\t')) idx++;
  239 + StringBuilder num = new StringBuilder();
  240 + while (idx < json.length() && (Character.isDigit(json.charAt(idx)) || json.charAt(idx) == '-')) {
  241 + num.append(json.charAt(idx));
  242 + idx++;
  243 + }
  244 + try { return Integer.parseInt(num.toString()); } catch (Throwable t) { return 0; }
  245 + }
  246 +
  247 + private static String parseJsonString(String json, String key) {
  248 + int idx = json.indexOf("\"" + key + "\"");
  249 + if (idx < 0) return "";
  250 + idx = json.indexOf("\"", idx + key.length() + 2);
  251 + if (idx < 0) return "";
  252 + int start = idx;
  253 + idx++;
  254 + while (idx < json.length()) {
  255 + char c = json.charAt(idx);
  256 + if (c == '\\') { idx += 2; continue; }
  257 + if (c == '"') break;
  258 + idx++;
  259 + }
  260 + return json.substring(start + 1, idx)
  261 + .replace("\\\"", "\"")
  262 + .replace("\\\\", "\\")
  263 + .replace("\\n", "\n");
  264 + }
  265 +
  266 + // ─── JSON builders ────────────────────────────────────────────
  267 +
  268 + private static String buildJson(String method, String url, String headersStr, String body) {
66 269 StringBuilder sb = new StringBuilder();
67 270 sb.append("{");
68 271 sb.append("\"method\":").append(jsonStr(method)).append(",");
69 272 sb.append("\"url\":").append(jsonStr(url)).append(",");
  273 + sb.append("\"body\":").append(jsonStr(body)).append(",");
70 274 sb.append("\"headers\":{");
71 275 boolean first = true;
72 276 for (String line : headersStr.split("\n")) {
... ... @@ -103,13 +307,10 @@ public class Hook implements IXposedHookLoadPackage {
103 307 try (OutputStream os = conn.getOutputStream()) {
104 308 os.write(body);
105 309 }
106   - int code = conn.getResponseCode();
  310 + conn.getResponseCode();
107 311 conn.disconnect();
108   - if (code != 200 && code != 201 && code != 204) {
109   - XposedBridge.log("[XHSHook] Hermes POST " + code);
110   - }
111 312 } catch (Throwable t) {
112   - // Hermes server not running — silent fail, logcat capture still works
  313 + // Hermes server not running — silent fail
113 314 }
114 315 }
115 316 }
... ...
xhs_lsposed_a16_fix/xhshook_v2_signed.apk
No preview for this file type