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,7 +219,7 @@ class XhsParams(BaseModel):
219 method: str 219 method: str
220 url: str 220 url: str
221 headers: dict[str, str] = {} 221 headers: dict[str, str] = {}
222 - 222 + body: str = ""
223 223
224 @app.post("/xhs_params", status_code=200) 224 @app.post("/xhs_params", status_code=200)
225 def post_xhs_params(params: XhsParams): 225 def post_xhs_params(params: XhsParams):
@@ -228,7 +228,10 @@ def post_xhs_params(params: XhsParams): @@ -228,7 +228,10 @@ def post_xhs_params(params: XhsParams):
228 with get_db() as conn: 228 with get_db() as conn:
229 cursor = conn.execute( 229 cursor = conn.execute(
230 "INSERT INTO events (type, app, data, ts, source) VALUES (?, ?, ?, ?, ?)", 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 conn.commit() 236 conn.commit()
234 return {"id": cursor.lastrowid} 237 return {"id": cursor.lastrowid}
@@ -243,3 +246,102 @@ def get_xhs_params(limit: int = Query(default=10, le=100)): @@ -243,3 +246,102 @@ def get_xhs_params(limit: int = Query(default=10, le=100)):
243 (limit,) 246 (limit,)
244 ).fetchall() 247 ).fetchall()
245 return [{"id": r["id"], "ts": r["ts"], **json.loads(r["data"])} for r in reversed(rows)] 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,20 +6,28 @@ import de.robv.android.xposed.XposedBridge;
6 import de.robv.android.xposed.XposedHelpers; 6 import de.robv.android.xposed.XposedHelpers;
7 import de.robv.android.xposed.callbacks.XC_LoadPackage; 7 import de.robv.android.xposed.callbacks.XC_LoadPackage;
8 8
  9 +import java.io.InputStream;
9 import java.io.OutputStream; 10 import java.io.OutputStream;
  11 +import java.io.ByteArrayOutputStream;
10 import java.net.HttpURLConnection; 12 import java.net.HttpURLConnection;
11 import java.net.URL; 13 import java.net.URL;
12 import java.nio.charset.StandardCharsets; 14 import java.nio.charset.StandardCharsets;
  15 +import java.util.Scanner;
13 16
14 public class Hook implements IXposedHookLoadPackage { 17 public class Hook implements IXposedHookLoadPackage {
15 - // Hermes event_server endpoint — POST xhs_params events here 18 + // Hermes event_server endpoints
16 private static final String HERMES_URL = "http://127.0.0.1:8765/xhs_params"; 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 @Override 25 @Override
19 public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { 26 public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
20 if (!lpparam.packageName.equals("com.xingin.xhs")) return; 27 if (!lpparam.packageName.equals("com.xingin.xhs")) return;
21 28
22 XposedBridge.log("[XHSHook] Loaded into XHS!"); 29 XposedBridge.log("[XHSHook] Loaded into XHS!");
  30 + appClassLoader = lpparam.classLoader;
23 31
24 try { 32 try {
25 Class<?> clz = XposedHelpers.findClass("okhttp3.Request$Builder", lpparam.classLoader); 33 Class<?> clz = XposedHelpers.findClass("okhttp3.Request$Builder", lpparam.classLoader);
@@ -42,13 +50,25 @@ public class Hook implements IXposedHookLoadPackage { @@ -42,13 +50,25 @@ public class Hook implements IXposedHookLoadPackage {
42 headersStr = headers != null ? headers.toString() : ""; 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 } catch (Throwable t) { 73 } catch (Throwable t) {
54 XposedBridge.log("[XHSHook] Error in hook: " + t.getMessage()); 74 XposedBridge.log("[XHSHook] Error in hook: " + t.getMessage());
@@ -56,17 +76,201 @@ public class Hook implements IXposedHookLoadPackage { @@ -56,17 +76,201 @@ public class Hook implements IXposedHookLoadPackage {
56 } 76 }
57 }); 77 });
58 XposedBridge.log("[XHSHook] OkHttp Request.Builder.build() hooked!"); 78 XposedBridge.log("[XHSHook] OkHttp Request.Builder.build() hooked!");
  79 +
  80 + // Start proxy poller in background
  81 + startProxyPoller();
  82 +
59 } catch (Throwable t) { 83 } catch (Throwable t) {
60 XposedBridge.log("[XHSHook] Failed to hook OkHttp: " + t.getMessage()); 84 XposedBridge.log("[XHSHook] Failed to hook OkHttp: " + t.getMessage());
61 XposedBridge.log(t); 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 StringBuilder sb = new StringBuilder(); 269 StringBuilder sb = new StringBuilder();
67 sb.append("{"); 270 sb.append("{");
68 sb.append("\"method\":").append(jsonStr(method)).append(","); 271 sb.append("\"method\":").append(jsonStr(method)).append(",");
69 sb.append("\"url\":").append(jsonStr(url)).append(","); 272 sb.append("\"url\":").append(jsonStr(url)).append(",");
  273 + sb.append("\"body\":").append(jsonStr(body)).append(",");
70 sb.append("\"headers\":{"); 274 sb.append("\"headers\":{");
71 boolean first = true; 275 boolean first = true;
72 for (String line : headersStr.split("\n")) { 276 for (String line : headersStr.split("\n")) {
@@ -103,13 +307,10 @@ public class Hook implements IXposedHookLoadPackage { @@ -103,13 +307,10 @@ public class Hook implements IXposedHookLoadPackage {
103 try (OutputStream os = conn.getOutputStream()) { 307 try (OutputStream os = conn.getOutputStream()) {
104 os.write(body); 308 os.write(body);
105 } 309 }
106 - int code = conn.getResponseCode(); 310 + conn.getResponseCode();
107 conn.disconnect(); 311 conn.disconnect();
108 - if (code != 200 && code != 201 && code != 204) {  
109 - XposedBridge.log("[XHSHook] Hermes POST " + code);  
110 - }  
111 } catch (Throwable t) { 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