package com.hermes.xhshook; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_LoadPackage; import java.io.InputStream; import java.io.OutputStream; import java.io.ByteArrayOutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Scanner; public class Hook implements IXposedHookLoadPackage { // Hermes event_server endpoints private static final String HERMES_URL = "http://127.0.0.1:8765/xhs_params"; private static final String PROXY_POLL_URL = "http://127.0.0.1:8765/xhs_proxy/pending"; private static final String PROXY_RESULT_URL = "http://127.0.0.1:8765/xhs_proxy/result"; private static ClassLoader appClassLoader; private static boolean proxyRunning = false; @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals("com.xingin.xhs")) return; XposedBridge.log("[XHSHook] Loaded into XHS!"); appClassLoader = lpparam.classLoader; try { Class clz = XposedHelpers.findClass("okhttp3.Request$Builder", lpparam.classLoader); XposedHelpers.findAndHookMethod(clz, "build", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { try { Object url = XposedHelpers.getObjectField(param.thisObject, "url"); String urlStr = url != null ? url.toString() : "null"; if (!urlStr.contains("edith.xiaohongshu.com")) return; Object method = XposedHelpers.getObjectField(param.thisObject, "method"); String methodStr = method != null ? method.toString() : "GET"; Object headersBuilder = XposedHelpers.getObjectField(param.thisObject, "headers"); String headersStr = ""; if (headersBuilder != null) { Object headers = XposedHelpers.callMethod(headersBuilder, "build"); headersStr = headers != null ? headers.toString() : ""; } // Capture request body (for POST/PUT requests) String bodyStr = ""; try { Object bodyObj = XposedHelpers.getObjectField(param.thisObject, "body"); if (bodyObj != null) { Class bufferClz = XposedHelpers.findClass("okio.Buffer", lpparam.classLoader); Object buffer = bufferClz.newInstance(); XposedHelpers.callMethod(bodyObj, "writeTo", buffer); bodyStr = (String) XposedHelpers.callMethod(buffer, "readUtf8"); if (bodyStr != null && bodyStr.length() > 2000) { bodyStr = bodyStr.substring(0, 2000); } } } catch (Throwable t) { bodyStr = ""; } final String json = buildJson(methodStr, urlStr, headersStr, bodyStr); new Thread(() -> postToHermes(json)).start(); } catch (Throwable t) { XposedBridge.log("[XHSHook] Error in hook: " + t.getMessage()); } } }); XposedBridge.log("[XHSHook] OkHttp Request.Builder.build() hooked!"); // Start proxy poller in background startProxyPoller(); } catch (Throwable t) { XposedBridge.log("[XHSHook] Failed to hook OkHttp: " + t.getMessage()); XposedBridge.log(t); } } // ─── Proxy Poller ───────────────────────────────────────────── private static void startProxyPoller() { if (proxyRunning) return; proxyRunning = true; new Thread(() -> { XposedBridge.log("[XHSHook] Proxy poller started"); while (true) { try { String json = httpGet(PROXY_POLL_URL); if (json != null && !json.equals("null") && !json.isEmpty()) { int proxyId = parseJsonInt(json, "proxy_id"); String reqUrl = parseJsonString(json, "url"); String method = parseJsonString(json, "method"); String body = parseJsonString(json, "body"); XposedBridge.log("[XHSHook] Proxy exec #" + proxyId + " " + method + " " + reqUrl); String[] result = executeOkHttp(proxyId, method, reqUrl, body); String status = result[0]; int httpCode = Integer.parseInt(result[1]); String respBody = result[2]; String error = result[3]; postProxyResult(proxyId, status, httpCode, respBody, error); } Thread.sleep(1000); } catch (Throwable t) { XposedBridge.log("[XHSHook] Proxy poller error: " + t.getMessage()); try { Thread.sleep(3000); } catch (Throwable tt) {} } } }).start(); } private static String[] executeOkHttp(int proxyId, String method, String url, String body) { // Returns [status, httpCode, responseBody, error] try { Class okHttpClz = XposedHelpers.findClass("okhttp3.OkHttpClient", appClassLoader); Class requestClz = XposedHelpers.findClass("okhttp3.Request", appClassLoader); Class requestBuilderClz = XposedHelpers.findClass("okhttp3.Request$Builder", appClassLoader); Class mediaTypeClz = XposedHelpers.findClass("okhttp3.MediaType", appClassLoader); Class requestBodyClz = XposedHelpers.findClass("okhttp3.RequestBody", appClassLoader); // Create OkHttpClient (new instance — auto-injects interceptors from XHS) Object client = okHttpClz.newInstance(); // Build request Object builder = requestBuilderClz.newInstance(); builder = XposedHelpers.callMethod(builder, "url", url); builder = XposedHelpers.callMethod(builder, "method", method, body != null && !body.isEmpty() ? XposedHelpers.callStaticMethod(requestBodyClz, "create", XposedHelpers.callStaticMethod(mediaTypeClz, "parse", "application/json; charset=utf-8"), body) : null); // Add standard XHS headers (User-Agent, Referer) builder = XposedHelpers.callMethod(builder, "addHeader", "User-Agent", "Dalvik/2.1.0 (Linux; U; Android 16; Pixel 10 Pro Fold Build/CP1A.260505.005) " + "Resolution/1080*2364 Version/9.35.1 Build/9351803 Device/(Google;Pixel 10 Pro Fold) " + "discover/9.35.1 NetType/WiFi"); builder = XposedHelpers.callMethod(builder, "addHeader", "Referer", "https://app.xhs.cn/"); Object request = XposedHelpers.callMethod(builder, "build"); // Execute Object call = XposedHelpers.callMethod(client, "newCall", request); Object response = XposedHelpers.callMethod(call, "execute"); int httpCode = (Integer) XposedHelpers.callMethod(response, "code"); Object respBodyObj = XposedHelpers.callMethod(response, "body"); Object source = XposedHelpers.callMethod(respBodyObj, "source"); Object buffer = XposedHelpers.findClass("okio.Buffer", appClassLoader).newInstance(); XposedHelpers.callMethod(source, "readAll", buffer); String respBody = (String) XposedHelpers.callMethod(buffer, "readUtf8"); // Truncate if (respBody != null && respBody.length() > 10000) { respBody = respBody.substring(0, 10000); } XposedHelpers.callMethod(response, "close"); return new String[]{"ok", String.valueOf(httpCode), respBody, ""}; } catch (Throwable t) { String errMsg = t.getMessage(); if (errMsg == null) errMsg = t.getClass().getName(); return new String[]{"error", "0", "", errMsg}; } } private static void postProxyResult(int proxyId, String status, int httpCode, String respBody, String error) { String json = "{\"proxy_id\":" + proxyId + ",\"status\":\"" + status + "\"" + ",\"http_code\":" + httpCode + ",\"response_body\":" + jsonStr(respBody) + ",\"error\":" + jsonStr(error) + "}"; try { HttpURLConnection conn = (HttpURLConnection) new URL(PROXY_RESULT_URL).openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); conn.setDoOutput(true); conn.setConnectTimeout(2000); conn.setReadTimeout(2000); byte[] bodyBytes = json.getBytes(StandardCharsets.UTF_8); conn.setFixedLengthStreamingMode(bodyBytes.length); try (OutputStream os = conn.getOutputStream()) { os.write(bodyBytes); } int code = conn.getResponseCode(); conn.disconnect(); if (code != 200) { XposedBridge.log("[XHSHook] Proxy result POST " + code); } } catch (Throwable t) { XposedBridge.log("[XHSHook] Failed to post proxy result: " + t.getMessage()); } } // ─── HTTP helpers ───────────────────────────────────────────── private static String httpGet(String urlStr) { try { HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(2000); conn.setReadTimeout(2000); int code = conn.getResponseCode(); if (code == 200) { Scanner s = new Scanner(conn.getInputStream(), "UTF-8").useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; s.close(); conn.disconnect(); return result; } conn.disconnect(); } catch (Throwable t) {} return null; } // ─── Minimal JSON parsing (no external deps) ────────────────── private static int parseJsonInt(String json, String key) { int idx = json.indexOf("\"" + key + "\""); if (idx < 0) return 0; idx = json.indexOf(":", idx); if (idx < 0) return 0; idx++; while (idx < json.length() && (json.charAt(idx) == ' ' || json.charAt(idx) == '\t')) idx++; StringBuilder num = new StringBuilder(); while (idx < json.length() && (Character.isDigit(json.charAt(idx)) || json.charAt(idx) == '-')) { num.append(json.charAt(idx)); idx++; } try { return Integer.parseInt(num.toString()); } catch (Throwable t) { return 0; } } private static String parseJsonString(String json, String key) { int idx = json.indexOf("\"" + key + "\""); if (idx < 0) return ""; idx = json.indexOf("\"", idx + key.length() + 2); if (idx < 0) return ""; int start = idx; idx++; while (idx < json.length()) { char c = json.charAt(idx); if (c == '\\') { idx += 2; continue; } if (c == '"') break; idx++; } return json.substring(start + 1, idx) .replace("\\\"", "\"") .replace("\\\\", "\\") .replace("\\n", "\n"); } // ─── JSON builders ──────────────────────────────────────────── private static String buildJson(String method, String url, String headersStr, String body) { StringBuilder sb = new StringBuilder(); sb.append("{"); sb.append("\"method\":").append(jsonStr(method)).append(","); sb.append("\"url\":").append(jsonStr(url)).append(","); sb.append("\"body\":").append(jsonStr(body)).append(","); sb.append("\"headers\":{"); boolean first = true; for (String line : headersStr.split("\n")) { int colon = line.indexOf(": "); if (colon < 0) continue; String k = line.substring(0, colon).trim(); String v = line.substring(colon + 2).trim(); if (!first) sb.append(","); sb.append(jsonStr(k)).append(":").append(jsonStr(v)); first = false; } sb.append("}}"); return sb.toString(); } private static String jsonStr(String s) { if (s == null) return "null"; return "\"" + s.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\n", "\\n") .replace("\r", "\\r") + "\""; } private static void postToHermes(String json) { try { HttpURLConnection conn = (HttpURLConnection) new URL(HERMES_URL).openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); conn.setDoOutput(true); conn.setConnectTimeout(1000); conn.setReadTimeout(1000); byte[] body = json.getBytes(StandardCharsets.UTF_8); conn.setFixedLengthStreamingMode(body.length); try (OutputStream os = conn.getOutputStream()) { os.write(body); } conn.getResponseCode(); conn.disconnect(); } catch (Throwable t) { // Hermes server not running — silent fail } } }