Hook.java 14.2 KB
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
        }
    }
}