diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 87ae4aa..9abd0f2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,10 +1,14 @@ @file:Suppress("UnstableApiUsage") +import java.io.BufferedReader import java.io.FileInputStream +import java.io.FileReader +import java.io.FileWriter import java.util.Properties plugins { id("com.android.application") + id("org.jetbrains.kotlin.android") } val keystorePropertiesFile = rootProject.file("keystore.properties") @@ -60,12 +64,176 @@ android { version = "3.22.1" } } + kotlinOptions { + jvmTarget = "11" + } +} + +task("generateDefaultOkHttp3Helper") { + group = "StethoX" + doFirst { + runCatching { + val okioTypes = listOf("BufferedSink", "Sink", "BufferedSource", "Okio", "Source", "BufferedSource") + fun String.toOkhttpClass() = + if (this.contains("_")) this.replace("_", ".") + else if (this in okioTypes) "okio.$this" + else "okhttp3.$this" + val f = + project.file("src/main/java/io/github/a13e300/tools/network/okhttp3/OkHttp3Helper.java") + FileWriter(project.file("src/main/java/io/github/a13e300/tools/network/okhttp3/DefaultOkHttp3Helper.java")).use { writer -> + BufferedReader(FileReader(f)).use { reader -> + val cstrContent = StringBuilder() + for (l in reader.lines()) { + if (l.startsWith("package") || l.startsWith("import")) { + writer.write(l) + } else if (l.startsWith("public interface")) { + writer.write("import java.lang.reflect.*;\n") + writer.write("import io.github.a13e300.tools.deobfuscation.NameMap;\n") + writer.write("public class DefaultOkHttp3Helper implements OkHttp3Helper {") + } else { + val trimmed = l.trim() + if (trimmed.startsWith("}") || trimmed.startsWith("//")) continue + val r = Regex("\\s*(.*)\\((.*)\\)(.*);").matchEntire(l) ?: continue + val typeAndName = r.groupValues[1].split(" ") + val arguments = r.groupValues[2] + val exceptions = r.groupValues[3].trim().let { + if (!it.startsWith("throws")) emptyList() + else it.removePrefix("throws").trim().split(Regex("\\s+,\\s+")) + } + var isStatic: Boolean = false + var returnType: String? = null + var className: String? = null + var methodName: String? = null + var functionName: String? = null + var isMethodGetter = false + var isClassGetter = false + var isFieldGetter = false + for (token in typeAndName) { + if (token == "/*static*/") { + isStatic = true + continue + } + if (returnType == null) { + returnType = token + if (token == "Method") isMethodGetter = true + if (token == "Class") isClassGetter = true + if (token == "Field") isFieldGetter = true + } else { + if (isClassGetter) { + className = token.toOkhttpClass() + functionName = token + } else Regex("(.*)_\\d*(.*?)").matchEntire(token)?.let { + className = it.groupValues[1].toOkhttpClass() + methodName = it.groupValues[2] + functionName = token + } + } + } + val argTypesAndNames = arguments.let { + if (isMethodGetter) it.removePrefix("/*").removeSuffix("*/") + else it + }.split(Regex(",\\s*")).mapNotNull { token -> + if (token.isEmpty()) return@mapNotNull null + token.split(" ").let { + it[it.lastIndex - 1].let { t -> + Regex("/\\*(.*)\\*/").matchEntire(t)?.groupValues?.get(1) + ?.let { t_ -> + t_.toOkhttpClass() to false + } ?: (t to true) + } to it.last() + } + } + if (isClassGetter) { + writer.write("private Class class_$functionName;\n") + writer.write("@Override public") + writer.write(l.removeSuffix(";") + " {\nreturn class_$functionName;\n}\n") + cstrContent.append("class_$functionName = classLoader.loadClass(nameMap.getClass(\"$className\"));\n") + continue + } else if (isFieldGetter) { + writer.write("private Field field_$functionName;\n") + writer.write("@Override public") + writer.write(l.removeSuffix(";") + " {\nreturn field_$functionName;\n}\n") + cstrContent.append("field_$functionName = classLoader.loadClass(nameMap.getClass(\"$className\")).getDeclaredField(nameMap.getField(\"$className\", \"$methodName\"));\n") + cstrContent.append("field_$functionName.setAccessible(true);\n") + continue + } + writer.write("private Method method_$functionName;\n") + writer.write("@Override public ") + writer.write(l.removeSuffix(";") + " {\n") + if (isMethodGetter) { + writer.write("return method_$functionName;\n}") + } else { + writer.write("try {\n${if (returnType == "void") "" else "return ($returnType)"}method_$functionName.invoke(") + val argList = mutableListOf() + if (isStatic) argList.add("null") + for ((_, name) in argTypesAndNames) { + argList.add(name) + } + writer.write(argList.joinToString(", ")) + writer.write(");\n}") + if (exceptions.isNotEmpty()) { + writer.write("catch (InvocationTargetException ex) {\n") + writer.write(exceptions.map { ex -> + "if (ex.getCause() instanceof $ex) { throw ($ex) ex.getCause(); }\n" + }.joinToString(" else ")) + writer.write(" else throw new RuntimeException(ex);\n}") + } + writer.write("catch (Throwable t) { throw new RuntimeException(t); }\n}") + } + cstrContent.append("method_$functionName = classLoader.loadClass(nameMap.getClass(\"$className\"))") + .append(".getDeclaredMethod(nameMap.getMethod(\"$className\", \"$methodName\"") + .apply { + var first = true + for ((t, _) in argTypesAndNames) { + if (!isStatic && first) { + first = false + continue + } + val (type, _) = t + append(", \"") + if (type == "String") append("java.lang.String") + else append(type) + append("\"") + } + append(")") + } + var first = true + for ((t, _) in argTypesAndNames) { + val (type, known) = t + if (!isStatic && first) { + first = false + continue + } + cstrContent.append(",") + if (known) + cstrContent.append("$type.class") + else + cstrContent.append("classLoader.loadClass(\"$type\")") + } + cstrContent.append(");\nmethod_$functionName.setAccessible(true);\n") + } + writer.write("\n") + } + writer.write("") + writer.write("public DefaultOkHttp3Helper(ClassLoader classLoader, NameMap nameMap) {\n") + writer.write("try {\n") + writer.write(cstrContent.toString()) + writer.write("} catch (Throwable t) {\nthrow new RuntimeException(t);\n}\n}\n}") + } + } + }.onFailure { it.printStackTrace() } + } } dependencies { + implementation("androidx.annotation:annotation:1.7.0") + implementation("androidx.core:core-ktx:+") compileOnly("de.robv.android.xposed:api:82") compileOnly(project(":hidden-api")) implementation("com.github.5ec1cff.stetho:stetho:1.0-alpha-1") implementation("com.github.5ec1cff.stetho:stetho-js-rhino:1.0-alpha-1") + implementation("com.github.5ec1cff.stetho:stetho-urlconnection:1.0-alpha-1") implementation("org.mozilla:rhino:1.7.15-SNAPSHOT") + implementation("com.linkedin.dexmaker:dexmaker:2.28.3") + implementation("org.luckypray:dexkit:2.0.0-rc4") } diff --git a/app/src/main/java/io/github/a13e300/tools/DexKitWrapper.java b/app/src/main/java/io/github/a13e300/tools/DexKitWrapper.java new file mode 100644 index 0000000..ec1c99c --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/DexKitWrapper.java @@ -0,0 +1,38 @@ +package io.github.a13e300.tools; + +import androidx.annotation.NonNull; + +import org.luckypray.dexkit.DexKitBridge; + +import java.util.Objects; + +public class DexKitWrapper { + + static { + try { + System.loadLibrary("dexkit"); + } catch (Throwable t) { + Logger.e("failed to load dexkit", t); + } + } + + @NonNull + public static DexKitBridge create(String apkPath) { + return Objects.requireNonNull(DexKitBridge.create(apkPath), "dexkit failed to create"); + } + + @NonNull + public static DexKitBridge create(ClassLoader cl) { + return Objects.requireNonNull(DexKitBridge.create(cl, true), "dexkit failed to create"); + } + + @NonNull + public static DexKitBridge create(ClassLoader cl, boolean useMemory) { + return Objects.requireNonNull(DexKitBridge.create(cl, useMemory), "dexkit failed to create"); + } + + @NonNull + public static DexKitBridge create(byte[][] dex) { + return Objects.requireNonNull(DexKitBridge.create(dex), "dexkit failed to create"); + } +} diff --git a/app/src/main/java/io/github/a13e300/tools/DexUtils.java b/app/src/main/java/io/github/a13e300/tools/DexUtils.java new file mode 100644 index 0000000..ddadea9 --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/DexUtils.java @@ -0,0 +1,73 @@ +package io.github.a13e300.tools; + + +import android.util.Log; + +import com.android.dx.DexMaker; +import com.android.dx.TypeId; + +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; + +import dalvik.system.InMemoryDexClassLoader; + +public class DexUtils { + public static void log(String msg) { + Logger.d("DexUtils log: " + msg); + } + public static Object make(ClassLoader classLoader, String name, String superName) { + var maker = new DexMaker(); + var superType = TypeId.get(superName); // LNeverCall$AB; + var myType = TypeId.get(name); // LMyAB; + maker.declare(myType, "Generated", Modifier.PUBLIC, superType); + var printMethod = myType.getMethod(TypeId.VOID, "print", TypeId.STRING); + var cstr = maker.declare(myType.getConstructor(), Modifier.PUBLIC); + var superCstr = superType.getConstructor(); + cstr.invokeDirect(superCstr, null, cstr.getThis(myType)); + cstr.returnVoid(); + var code = maker.declare(printMethod, Modifier.PUBLIC); + var tag = code.newLocal(TypeId.STRING); + code.loadConstant(tag, "DexUtils"); + var p0 = code.getParameter(0, TypeId.STRING); + var logI = TypeId.get(Log.class).getMethod(TypeId.INT, "i", TypeId.STRING, TypeId.STRING); + var myLog = TypeId.get(DexUtils.class).getMethod(TypeId.VOID, "log", TypeId.STRING); + code.invokeStatic(logI, null, tag, p0); + code.invokeStatic(myLog, null, p0); + code.returnVoid(); + var dex = maker.generate(); + try { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) return null; + var loader = new InMemoryDexClassLoader(ByteBuffer.wrap(dex), new HybridClassLoader(classLoader)); + var clazz = loader.loadClass("MyAB"); + Logger.d("class loader=" + clazz.getClassLoader()); + var superClass = classLoader.loadClass("NeverCall$AB"); + var inst = clazz.newInstance(); + var m = superClass.getDeclaredMethod("call", superClass, String.class); + m.setAccessible(true); + m.invoke(null, inst, "test"); + return inst; + } catch (Throwable t) { + Logger.e("dexutils load and call", t); + } + return null; + } + + static class HybridClassLoader extends ClassLoader { + private final ClassLoader mBase; + public HybridClassLoader(ClassLoader parent) { + mBase = parent; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("io.github.a13e300.tools")) { + return getClass().getClassLoader().loadClass(name); + } + try { + return mBase.loadClass(name); + } catch (ClassNotFoundException e) { + return super.loadClass(name, resolve); + } + } + } +} diff --git a/app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java b/app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java index 2eaa44a..c742fd0 100644 --- a/app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java +++ b/app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java @@ -16,6 +16,8 @@ import com.facebook.stetho.Stetho; import com.facebook.stetho.rhino.JsRuntimeReplFactoryBuilder; +import org.mozilla.javascript.BaseFunction; +import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import java.lang.reflect.InvocationTargetException; @@ -28,6 +30,7 @@ import io.github.a13e300.tools.objects.GetStackTraceFunction; import io.github.a13e300.tools.objects.HookFunction; import io.github.a13e300.tools.objects.HookParam; +import io.github.a13e300.tools.objects.OkHttpInterceptorObject; import io.github.a13e300.tools.objects.PrintStackTraceFunction; import io.github.a13e300.tools.objects.RunOnHandlerFunction; import io.github.a13e300.tools.objects.UnhookFunction; @@ -82,7 +85,9 @@ private synchronized void initializeStetho(Context context) throws InterruptedEx ScriptableObject.defineClass(scope, HookFunction.class); ScriptableObject.defineClass(scope, UnhookFunction.class); ScriptableObject.defineClass(scope, HookParam.class); + ScriptableObject.defineClass(scope, OkHttpInterceptorObject.class); scope.defineProperty("hook", new HookFunction(scope), ScriptableObject.DONTENUM | ScriptableObject.CONST); + scope.defineProperty("okhttp3", new OkHttpInterceptorObject(scope), ScriptableObject.DONTENUM | ScriptableObject.CONST); synchronized (StethoxAppInterceptor.this) { if (mWaiting) { var method = StethoxAppInterceptor.class.getDeclaredMethod("cont", ScriptableObject.class); @@ -99,6 +104,19 @@ private synchronized void initializeStetho(Context context) throws InterruptedEx }) .onFinalize(scope -> { ((HookFunction) ScriptableObject.getProperty(scope, "hook")).clearHooks(); + ((OkHttpInterceptorObject) ScriptableObject.getProperty(scope, "okhttp3")).stop(true); + }) + // .importClass(DexUtils.class) + // .importClass(StethoOkHttp3ProxyInterceptor.class) + // .importClass(MutableNameMap.class) + // .importClass(DexKitWrapper.class) + // .importPackage("org.luckypray.dexkit.query") + // .importPackage("org.luckypray.dexkit.query.matchers") + .addFunction("MYCL", new BaseFunction() { + @Override + public Object call(org.mozilla.javascript.Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return StethoxAppInterceptor.class.getClassLoader(); + } }) .build() ).finish() diff --git a/app/src/main/java/io/github/a13e300/tools/deobfuscation/MutableNameMap.java b/app/src/main/java/io/github/a13e300/tools/deobfuscation/MutableNameMap.java new file mode 100644 index 0000000..2cebf99 --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/deobfuscation/MutableNameMap.java @@ -0,0 +1,56 @@ +package io.github.a13e300.tools.deobfuscation; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MutableNameMap implements NameMap { + private static class ClassNode { + String name; + final Map, String>> methodMap = new HashMap<>(); + final Map fieldMap = new HashMap<>(); + } + private final Map mClassMap = new HashMap<>(); + @Override + public String getClass(String name) { + var node = mClassMap.get(name); + if (node == null) return name; + return node.name; + } + + public void putClass(String name, String obfName) { + mClassMap.computeIfAbsent(name, k -> new ClassNode()).name = obfName; + } + + @Override + public String getMethod(String className, String methodName, String... argTypes) { + var node = mClassMap.get(className); + if (node == null) return methodName; + var mm = node.methodMap.get(methodName); + if (mm == null) return methodName; + var n = mm.get(Arrays.asList(argTypes)); + if (n == null) return methodName; + return n; + } + + public void putMethod(String className, String methodName, String obfName, String... argTypes) { + mClassMap.computeIfAbsent(className, k -> new ClassNode()) + .methodMap.computeIfAbsent(methodName, k -> new HashMap<>()) + .put(Arrays.asList(argTypes), obfName); + } + + @Override + public String getField(String className, String fieldName) { + var node = mClassMap.get(className); + if (node == null) return fieldName; + var n = node.fieldMap.get(fieldName); + if (n == null) return fieldName; + return n; + } + + public void putField(String className, String fieldName, String obfName) { + mClassMap.computeIfAbsent(className, k -> new ClassNode()) + .fieldMap.put(fieldName, obfName); + } +} diff --git a/app/src/main/java/io/github/a13e300/tools/deobfuscation/NameMap.java b/app/src/main/java/io/github/a13e300/tools/deobfuscation/NameMap.java new file mode 100644 index 0000000..a1e8f1d --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/deobfuscation/NameMap.java @@ -0,0 +1,23 @@ +package io.github.a13e300.tools.deobfuscation; + +public interface NameMap { + NameMap sDefaultMap = new NameMap() { + @Override + public String getClass(String name) { + return name; + } + + @Override + public String getMethod(String className, String methodName, String... argTypes) { + return methodName; + } + + @Override + public String getField(String className, String fieldName) { + return fieldName; + } + }; + String getClass(String name); + String getMethod(String className, String methodName, String... argTypes); + String getField(String className, String fieldName); +} diff --git a/app/src/main/java/io/github/a13e300/tools/network/httpurl/HttpURLConnectionInterceptor.java b/app/src/main/java/io/github/a13e300/tools/network/httpurl/HttpURLConnectionInterceptor.java new file mode 100644 index 0000000..932c449 --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/network/httpurl/HttpURLConnectionInterceptor.java @@ -0,0 +1,132 @@ +package io.github.a13e300.tools.network.httpurl; + +import com.facebook.stetho.urlconnection.StethoURLConnectionManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URL; +import java.security.Permission; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedHelpers; + +public class HttpURLConnectionInterceptor { + private static XC_MethodHook.Unhook sUnhook; + public static synchronized void enable() { + sUnhook = XposedHelpers.findAndHookMethod(URL.class, "openConnection", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + var connection = param.getResult(); + if (!(connection instanceof HttpURLConnection)) return; + + } + }); + } + + public static synchronized void disable() { + if (sUnhook != null) { + sUnhook.unhook(); + sUnhook = null; + } + } + + private class HttpURLConnectionWrapper extends HttpURLConnection { + private final HttpURLConnection mWrapped; + private final StethoURLConnectionManager mManager; + private ByteArrayOutputStream mOutputStream; + HttpURLConnectionWrapper(HttpURLConnection wrapped) { + super(wrapped.getURL()); + mWrapped = wrapped; + mManager = new StethoURLConnectionManager(null); + } + + @Override + public void connect() throws IOException { + + } + + @Override + public boolean getDoOutput() { + return mWrapped.getDoOutput(); + } + + @Override + public void setDoOutput(boolean dooutput) { + mWrapped.setDoOutput(dooutput); + } + + @Override + public OutputStream getOutputStream() throws IOException { + // TODO + return mWrapped.getOutputStream(); + } + + public String getHeaderFieldKey(int n) { + return mWrapped.getHeaderFieldKey(n); + } + + public void setFixedLengthStreamingMode(int contentLength) { + mWrapped.setFixedLengthStreamingMode(contentLength); + } + + public void setFixedLengthStreamingMode(long contentLength) { + mWrapped.setFixedLengthStreamingMode(contentLength); + } + + public void setChunkedStreamingMode(int chunklen) { + mWrapped.setChunkedStreamingMode(chunklen); + } + + public String getHeaderField(int n) { + return mWrapped.getHeaderField(n); + } + + public void setInstanceFollowRedirects(boolean followRedirects) { + mWrapped.setInstanceFollowRedirects(followRedirects); + } + + public boolean getInstanceFollowRedirects() { + return mWrapped.getInstanceFollowRedirects(); + } + + public void setRequestMethod(String method) throws ProtocolException { + mWrapped.setRequestMethod(method); + } + + public String getRequestMethod() { + return mWrapped.getRequestMethod(); + } + + public int getResponseCode() throws IOException { + return mWrapped.getResponseCode(); + } + + public String getResponseMessage() throws IOException { + return mWrapped.getResponseMessage(); + } + + public long getHeaderFieldDate(String name, long Default) { + return mWrapped.getHeaderFieldDate(name, Default); + } + + public void disconnect() { + mWrapped.disconnect(); + } + + public boolean usingProxy() { + return mWrapped.usingProxy(); + } + + public Permission getPermission() throws IOException { + return mWrapped.getPermission(); + } + + public InputStream getErrorStream() { + return mWrapped.getErrorStream(); + } + } +} diff --git a/app/src/main/java/io/github/a13e300/tools/network/okhttp3/DefaultOkHttp3Helper.java b/app/src/main/java/io/github/a13e300/tools/network/okhttp3/DefaultOkHttp3Helper.java new file mode 100644 index 0000000..0733782 --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/network/okhttp3/DefaultOkHttp3Helper.java @@ -0,0 +1,430 @@ +package io.github.a13e300.tools.network.okhttp3; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import io.github.a13e300.tools.deobfuscation.NameMap; + +public class DefaultOkHttp3Helper implements OkHttp3Helper { + private Method method_Interceptor$Chain_request; + + @Override + public Object /*Request*/ Interceptor$Chain_request(Object chain) { + try { + return (Object) method_Interceptor$Chain_request.invoke(chain); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Interceptor$Chain_proceed; + + @Override + public Object /*Response*/ Interceptor$Chain_proceed(Object chain, Object /*Request*/ request) throws IOException { + try { + return (Object) method_Interceptor$Chain_proceed.invoke(chain, request); + } catch (InvocationTargetException ex) { + if (ex.getCause() instanceof IOException) { + throw (IOException) ex.getCause(); + } else throw new RuntimeException(ex); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Interceptor$Chain_connection; + + @Override + public Object /*Connection*/ Interceptor$Chain_connection(Object chain) { + try { + return (Object) method_Interceptor$Chain_connection.invoke(chain); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response_body; + + @Override + public Object /*ResponseBody*/ Response_body(Object response) { + try { + return (Object) method_Response_body.invoke(response); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response_header; + + @Override + public String Response_header(Object response, String name) { + try { + return (String) method_Response_header.invoke(response, name); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response_headers; + + @Override + public Object /*Headers*/ Response_headers(Object response) { + try { + return (Object) method_Response_headers.invoke(response); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response_cacheResponse; + + @Override + public Object /*Response*/ Response_cacheResponse(Object response) { + try { + return (Object) method_Response_cacheResponse.invoke(response); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response_newBuilder; + + @Override + public Object /*Response$Builder*/ Response_newBuilder(Object response) { + try { + return (Object) method_Response_newBuilder.invoke(response); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response$Builder_build; + + @Override + public Object /*Response*/ Response$Builder_build(Object builder) { + try { + return (Object) method_Response$Builder_build.invoke(builder); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_ResponseBody_create; + + @Override + public /*static*/ Object /*ResponseBody*/ ResponseBody_create(Object /*MediaType*/ contentType, long contentLength, Object /*BufferedSource*/ content) { + try { + return (Object) method_ResponseBody_create.invoke(null, contentType, contentLength, content); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response$Builder_body; + + @Override + public Object /*Response$Builder*/ Response$Builder_body(Object builder, Object /*ResponseBody*/ body) { + try { + return (Object) method_Response$Builder_body.invoke(builder, body); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_ResponseBody_contentType; + + @Override + public Object /*MediaType*/ ResponseBody_contentType(Object body) { + try { + return (Object) method_ResponseBody_contentType.invoke(body); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_ResponseBody_byteStream; + + @Override + public InputStream ResponseBody_byteStream(Object body) { + try { + return (InputStream) method_ResponseBody_byteStream.invoke(body); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_ResponseBody_contentLength; + + @Override + public long ResponseBody_contentLength(Object body) { + try { + return (long) method_ResponseBody_contentLength.invoke(body); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Request_url; + + @Override + public Object /*HttpUrl*/ Request_url(Object request) { + try { + return (Object) method_Request_url.invoke(request); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Request_method; + + @Override + public String Request_method(Object request) { + try { + return (String) method_Request_method.invoke(request); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Request_body; + + @Override + public Object /*RequestBody*/ Request_body(Object request) { + try { + return (Object) method_Request_body.invoke(request); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Request_headers; + + @Override + public Object /*Headers*/ Request_headers(Object request) { + try { + return (Object) method_Request_headers.invoke(request); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Request_header; + + @Override + public String Request_header(Object request, String name) { + try { + return (String) method_Request_header.invoke(request, name); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_RequestBody_writeTo; + + @Override + public void RequestBody_writeTo(Object body, Closeable /*BufferedSink*/ bufferedSink) throws IOException { + try { + method_RequestBody_writeTo.invoke(body, bufferedSink); + } catch (InvocationTargetException ex) { + if (ex.getCause() instanceof IOException) { + throw (IOException) ex.getCause(); + } else throw new RuntimeException(ex); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Headers_size; + + @Override + public int Headers_size(Object headers) { + try { + return (int) method_Headers_size.invoke(headers); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Headers_name; + + @Override + public String Headers_name(Object headers, int index) { + try { + return (String) method_Headers_name.invoke(headers, index); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Headers_value; + + @Override + public String Headers_value(Object headers, int index) { + try { + return (String) method_Headers_value.invoke(headers, index); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response_code; + + @Override + public int Response_code(Object response) { + try { + return (int) method_Response_code.invoke(response); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Response_message; + + @Override + public String Response_message(Object response) { + try { + return (String) method_Response_message.invoke(response); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Okio_sink; + + @Override + public /*static*/ Closeable /*Sink*/ Okio_sink(OutputStream out) { + try { + return (Closeable) method_Okio_sink.invoke(null, out); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Okio_buffer; + + @Override + public /*static*/ Closeable /*BufferedSink*/ Okio_buffer(Closeable /*Sink*/ sink) { + try { + return (Closeable) method_Okio_buffer.invoke(null, sink); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Okio_source; + + @Override + public /*static*/ Closeable /*Source*/ Okio_source(InputStream in) { + try { + return (Closeable) method_Okio_source.invoke(null, in); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Method method_Okio_1buffer; + + @Override + public /*static*/ Closeable /*BufferedSource*/ Okio_1buffer(Closeable /*Source*/ source) { + try { + return (Closeable) method_Okio_1buffer.invoke(null, source); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + private Class class_Interceptor; + + @Override + public Class Interceptor() { + return class_Interceptor; + } + + private Class class_okhttp3_internal_http_RealInterceptorChain; + + @Override + public Class okhttp3_internal_http_RealInterceptorChain() { + return class_okhttp3_internal_http_RealInterceptorChain; + } + + private Field field_okhttp3_internal_http_RealInterceptorChain_interceptors; + + @Override + public Field okhttp3_internal_http_RealInterceptorChain_interceptors() { + return field_okhttp3_internal_http_RealInterceptorChain_interceptors; + } + + public DefaultOkHttp3Helper(ClassLoader classLoader, NameMap nameMap) { + try { + method_Interceptor$Chain_request = classLoader.loadClass(nameMap.getClass("okhttp3.Interceptor$Chain")).getDeclaredMethod(nameMap.getMethod("okhttp3.Interceptor$Chain", "request")); + method_Interceptor$Chain_request.setAccessible(true); + method_Interceptor$Chain_proceed = classLoader.loadClass(nameMap.getClass("okhttp3.Interceptor$Chain")).getDeclaredMethod(nameMap.getMethod("okhttp3.Interceptor$Chain", "proceed", "okhttp3.Request"), classLoader.loadClass("okhttp3.Request")); + method_Interceptor$Chain_proceed.setAccessible(true); + method_Interceptor$Chain_connection = classLoader.loadClass(nameMap.getClass("okhttp3.Interceptor$Chain")).getDeclaredMethod(nameMap.getMethod("okhttp3.Interceptor$Chain", "connection")); + method_Interceptor$Chain_connection.setAccessible(true); + method_Response_body = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "body")); + method_Response_body.setAccessible(true); + method_Response_header = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "header", "java.lang.String"), String.class); + method_Response_header.setAccessible(true); + method_Response_headers = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "headers")); + method_Response_headers.setAccessible(true); + method_Response_cacheResponse = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "cacheResponse")); + method_Response_cacheResponse.setAccessible(true); + method_Response_newBuilder = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "newBuilder")); + method_Response_newBuilder.setAccessible(true); + method_Response$Builder_build = classLoader.loadClass(nameMap.getClass("okhttp3.Response$Builder")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response$Builder", "build")); + method_Response$Builder_build.setAccessible(true); + method_ResponseBody_create = classLoader.loadClass(nameMap.getClass("okhttp3.ResponseBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.ResponseBody", "create", "okhttp3.MediaType", "long", "okio.BufferedSource"), classLoader.loadClass("okhttp3.MediaType"), long.class, classLoader.loadClass("okio.BufferedSource")); + method_ResponseBody_create.setAccessible(true); + method_Response$Builder_body = classLoader.loadClass(nameMap.getClass("okhttp3.Response$Builder")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response$Builder", "body", "okhttp3.ResponseBody"), classLoader.loadClass("okhttp3.ResponseBody")); + method_Response$Builder_body.setAccessible(true); + method_ResponseBody_contentType = classLoader.loadClass(nameMap.getClass("okhttp3.ResponseBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.ResponseBody", "contentType")); + method_ResponseBody_contentType.setAccessible(true); + method_ResponseBody_byteStream = classLoader.loadClass(nameMap.getClass("okhttp3.ResponseBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.ResponseBody", "byteStream")); + method_ResponseBody_byteStream.setAccessible(true); + method_ResponseBody_contentLength = classLoader.loadClass(nameMap.getClass("okhttp3.ResponseBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.ResponseBody", "contentLength")); + method_ResponseBody_contentLength.setAccessible(true); + method_Request_url = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "url")); + method_Request_url.setAccessible(true); + method_Request_method = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "method")); + method_Request_method.setAccessible(true); + method_Request_body = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "body")); + method_Request_body.setAccessible(true); + method_Request_headers = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "headers")); + method_Request_headers.setAccessible(true); + method_Request_header = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "header", "java.lang.String"), String.class); + method_Request_header.setAccessible(true); + method_RequestBody_writeTo = classLoader.loadClass(nameMap.getClass("okhttp3.RequestBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.RequestBody", "writeTo", "okio.BufferedSink"), classLoader.loadClass("okio.BufferedSink")); + method_RequestBody_writeTo.setAccessible(true); + method_Headers_size = classLoader.loadClass(nameMap.getClass("okhttp3.Headers")).getDeclaredMethod(nameMap.getMethod("okhttp3.Headers", "size")); + method_Headers_size.setAccessible(true); + method_Headers_name = classLoader.loadClass(nameMap.getClass("okhttp3.Headers")).getDeclaredMethod(nameMap.getMethod("okhttp3.Headers", "name", "int"), int.class); + method_Headers_name.setAccessible(true); + method_Headers_value = classLoader.loadClass(nameMap.getClass("okhttp3.Headers")).getDeclaredMethod(nameMap.getMethod("okhttp3.Headers", "value", "int"), int.class); + method_Headers_value.setAccessible(true); + method_Response_code = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "code")); + method_Response_code.setAccessible(true); + method_Response_message = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "message")); + method_Response_message.setAccessible(true); + method_Okio_sink = classLoader.loadClass(nameMap.getClass("okio.Okio")).getDeclaredMethod(nameMap.getMethod("okio.Okio", "sink", "OutputStream"), OutputStream.class); + method_Okio_sink.setAccessible(true); + method_Okio_buffer = classLoader.loadClass(nameMap.getClass("okio.Okio")).getDeclaredMethod(nameMap.getMethod("okio.Okio", "buffer", "okio.Sink"), classLoader.loadClass("okio.Sink")); + method_Okio_buffer.setAccessible(true); + method_Okio_source = classLoader.loadClass(nameMap.getClass("okio.Okio")).getDeclaredMethod(nameMap.getMethod("okio.Okio", "source", "InputStream"), InputStream.class); + method_Okio_source.setAccessible(true); + method_Okio_1buffer = classLoader.loadClass(nameMap.getClass("okio.Okio")).getDeclaredMethod(nameMap.getMethod("okio.Okio", "buffer", "okio.Source"), classLoader.loadClass("okio.Source")); + method_Okio_1buffer.setAccessible(true); + class_Interceptor = classLoader.loadClass(nameMap.getClass("okhttp3.Interceptor")); + class_okhttp3_internal_http_RealInterceptorChain = classLoader.loadClass(nameMap.getClass("okhttp3.internal.http.RealInterceptorChain")); + field_okhttp3_internal_http_RealInterceptorChain_interceptors = classLoader.loadClass(nameMap.getClass("okhttp3.internal.http.RealInterceptorChain")).getDeclaredField(nameMap.getField("okhttp3.internal.http.RealInterceptorChain", "interceptors")); + field_okhttp3_internal_http_RealInterceptorChain_interceptors.setAccessible(true); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/a13e300/tools/network/okhttp3/DeobfuscationUtils.kt b/app/src/main/java/io/github/a13e300/tools/network/okhttp3/DeobfuscationUtils.kt new file mode 100644 index 0000000..10f9bcc --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/network/okhttp3/DeobfuscationUtils.kt @@ -0,0 +1,26 @@ +package io.github.a13e300.tools.network.okhttp3 + +import io.github.a13e300.tools.DexKitWrapper +import io.github.a13e300.tools.deobfuscation.MutableNameMap +import io.github.a13e300.tools.deobfuscation.NameMap + +fun deobfOkhttp3(cl: ClassLoader): NameMap { + val map = MutableNameMap() + DexKitWrapper.create(cl).use { + val classRealInterceptorChain = it.findClass { + matcher { + usingStrings = listOf("must call proceed() exactly once", "returned a response with no body") + } + } + require(classRealInterceptorChain.size == 1) { "require only one RealInterceptorChain, found ${classRealInterceptorChain.size}" } + val fieldInterceptors = classRealInterceptorChain[0].getFields().findField { + matcher { + type = "java.util.List" + } + } + require(fieldInterceptors.size == 1) { "require only one Interceptors field, found ${fieldInterceptors.size}" } + map.putClass("okhttp3.internal.http.RealInterceptorChain", classRealInterceptorChain[0].className) + map.putField("okhttp3.internal.http.RealInterceptorChain", "interceptors", fieldInterceptors[0].name) + } + return map +} diff --git a/app/src/main/java/io/github/a13e300/tools/network/okhttp3/OkHttp3Helper.java b/app/src/main/java/io/github/a13e300/tools/network/okhttp3/OkHttp3Helper.java new file mode 100644 index 0000000..2f7353b --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/network/okhttp3/OkHttp3Helper.java @@ -0,0 +1,55 @@ +package io.github.a13e300.tools.network.okhttp3; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; + +public interface OkHttp3Helper { + Object /*Request*/ Interceptor$Chain_request(Object chain); + Object /*Response*/ Interceptor$Chain_proceed(Object chain, Object /*Request*/ request) throws IOException; + Object /*Connection*/ Interceptor$Chain_connection(Object chain); + + + Object /*ResponseBody*/ Response_body(Object response); + String Response_header(Object response, String name); + Object /*Headers*/ Response_headers(Object response); + Object /*Response*/ Response_cacheResponse(Object response); + Object /*Response$Builder*/ Response_newBuilder(Object response); + Object /*Response*/ Response$Builder_build(Object builder); + /*static*/ Object /*ResponseBody*/ ResponseBody_create(Object /*MediaType*/ contentType, long contentLength, Object /*BufferedSource*/ content); + Object /*Response$Builder*/ Response$Builder_body(Object builder, Object /*ResponseBody*/ body); + + + Object /*MediaType*/ ResponseBody_contentType(Object body); + InputStream ResponseBody_byteStream(Object body); + long ResponseBody_contentLength(Object body); + + Object /*HttpUrl*/ Request_url(Object request); + String Request_method(Object request); + Object /*RequestBody*/ Request_body(Object request); + Object /*Headers*/ Request_headers(Object request); + String Request_header(Object request, String name); + + void RequestBody_writeTo(Object body, Closeable /*BufferedSink*/ bufferedSink) throws IOException; + + int Headers_size(Object headers); + String Headers_name(Object headers, int index); + String Headers_value(Object headers, int index); + + int Response_code(Object response); + String Response_message(Object response); + + /*static*/ Closeable /*Sink*/ Okio_sink(OutputStream out); + /*static*/ Closeable /*BufferedSink*/ Okio_buffer(Closeable /*Sink*/ sink); + /*static*/ Closeable /*Source*/ Okio_source(InputStream in); + /*static*/ Closeable /*BufferedSource*/ Okio_1buffer(Closeable /*Source*/ source); + + + + Class Interceptor(); + + Class okhttp3_internal_http_RealInterceptorChain(); + Field okhttp3_internal_http_RealInterceptorChain_interceptors(); +} diff --git a/app/src/main/java/io/github/a13e300/tools/network/okhttp3/StethoOkHttp3ProxyInterceptor.java b/app/src/main/java/io/github/a13e300/tools/network/okhttp3/StethoOkHttp3ProxyInterceptor.java new file mode 100644 index 0000000..3865389 --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/network/okhttp3/StethoOkHttp3ProxyInterceptor.java @@ -0,0 +1,347 @@ +package io.github.a13e300.tools.network.okhttp3; + +import androidx.annotation.Nullable; + +import com.facebook.stetho.inspector.network.DefaultResponseHandler; +import com.facebook.stetho.inspector.network.NetworkEventReporter; +import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; +import com.facebook.stetho.inspector.network.RequestBodyHelper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import io.github.a13e300.tools.Logger; +import io.github.a13e300.tools.deobfuscation.NameMap; + + + +public class StethoOkHttp3ProxyInterceptor { + /*public static XC_MethodHook.Unhook startHookClientBuild(ClassLoader classLoader) { + OkHttp3Helper helper = new DefaultOkHttp3Helper(classLoader, NameMap.sDefaultMap); + var interceptor = new StethoOkHttp3ProxyInterceptor(helper); + var proxy = Proxy.newProxyInstance(classLoader, new Class[]{helper.Interceptor()}, + (o, method, objects) -> { + if ("intercept".equals(method.getName())) + return interceptor.intercept(objects[0]); + try { + return method.invoke(o, objects); + } catch (InvocationTargetException e) { + if (e.getCause() != null) throw e.getCause(); + throw new RuntimeException(e); + } + } + ); + return XposedBridge.hookMethod(helper.OkHttpClient$Builder_getNetworkInterceptors$okhttp(), new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + var list = (List) param.getResult(); + if (list == null) list = new ArrayList(); + if (list.contains(proxy)) return; + list.add(proxy); + param.setResult(list); + } + }); + return null; + }*/ + public static Set start(ClassLoader classLoader) { + try { + return start(classLoader, false); + } catch (Throwable t) { + Logger.e("try deobfuscation for okhttp"); + return start(classLoader, true); + } + } + + public static Set start(ClassLoader classLoader, boolean useDeObf) { + if (useDeObf) { + return start(classLoader, DeobfuscationUtilsKt.deobfOkhttp3(classLoader)); + } else { + return start(classLoader, NameMap.sDefaultMap); + } + } + + public static Set start(ClassLoader classLoader, NameMap nameMap) { + OkHttp3Helper helper = new DefaultOkHttp3Helper(classLoader, nameMap); + var interceptor = new StethoOkHttp3ProxyInterceptor(helper); + var proxy = Proxy.newProxyInstance(classLoader, new Class[]{helper.Interceptor()}, + (o, method, objects) -> { + if ("intercept".equals(method.getName())) { + return interceptor.intercept(objects[0]); + } else if ("equals".equals(method.getName())) { + return o == objects[0]; + } else if ("hashCode".equals(method.getName())) { + return System.identityHashCode(o); + } else if ("toString".equals(method.getName())) { + return o.getClass().getName() + "{" + System.identityHashCode(o) + "}"; + } else { + throw new IllegalArgumentException("unknown method " + method + " to proxy!"); + } + } + ); + return XposedBridge.hookAllConstructors(helper.okhttp3_internal_http_RealInterceptorChain(), new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + // TODO: avoid using proxy + var oldList = (List) helper.okhttp3_internal_http_RealInterceptorChain_interceptors().get(param.thisObject); + if (oldList != null) { + if (oldList.contains(proxy)) return; + var list = new ArrayList(); + list.addAll(oldList); + list.add(list.size() - 1, proxy); + Logger.d("add " + proxy + " to " + list); + helper.okhttp3_internal_http_RealInterceptorChain_interceptors().set(param.thisObject, list); + var updatedList = helper.okhttp3_internal_http_RealInterceptorChain_interceptors().get(param.thisObject); + Logger.d("updated=" + updatedList); + } + } + }); + } + + private final NetworkEventReporter mEventReporter = NetworkEventReporterImpl.get(); + + private final OkHttp3Helper mHelper; + + public StethoOkHttp3ProxyInterceptor(OkHttp3Helper helper) { + mHelper = helper; + } + + private Object /*Response*/ intercept(Object /*Chain*/ chain) throws IOException { + Logger.d("intercept " + chain); + String requestId = mEventReporter.nextRequestId(); + + Object /*Request*/ request = mHelper.Interceptor$Chain_request(chain); + + RequestBodyHelper requestBodyHelper = null; + if (mEventReporter.isEnabled()) { + requestBodyHelper = new RequestBodyHelper(mEventReporter, requestId); + OkHttpInspectorRequest inspectorRequest = + new OkHttpInspectorRequest(mHelper, requestId, request, requestBodyHelper); + mEventReporter.requestWillBeSent(inspectorRequest); + } + + Object /*Response*/ response; + try { + response = mHelper.Interceptor$Chain_proceed(chain, request); + } catch (IOException e) { + if (mEventReporter.isEnabled()) { + mEventReporter.httpExchangeFailed(requestId, e.toString()); + } + throw e; + } + + if (mEventReporter.isEnabled()) { + if (requestBodyHelper != null && requestBodyHelper.hasBody()) { + requestBodyHelper.reportDataSent(); + } + + Object /*Connection*/ connection = mHelper.Interceptor$Chain_connection(chain); + if (connection == null) { + throw new IllegalStateException( + "No connection associated with this request; " + + "did you use addInterceptor instead of addNetworkInterceptor?"); + } + mEventReporter.responseHeadersReceived( + new OkHttpInspectorResponse( + mHelper, + requestId, + request, + response, + connection)); + + Object /*ResponseBody*/ body = mHelper.Response_body(response); + Object /*MediaType*/ contentType = null; + InputStream responseStream = null; + if (body != null) { + contentType = mHelper.ResponseBody_contentType(body); + responseStream = mHelper.ResponseBody_byteStream(body); + } + + responseStream = mEventReporter.interpretResponseStream( + requestId, + contentType != null ? contentType.toString() : null, + mHelper.Response_header(response, "Content-Encoding"), + responseStream, + new DefaultResponseHandler(mEventReporter, requestId)); + if (responseStream != null) { + var responseBody = mHelper.ResponseBody_create( + mHelper.ResponseBody_contentType(body), + mHelper.ResponseBody_contentLength(body), + mHelper.Okio_1buffer(mHelper.Okio_source(responseStream)) + ); + response = mHelper.Response$Builder_build( + mHelper.Response$Builder_body( + mHelper.Response_newBuilder(response), responseBody + ) + ); + } + } + + return response; + } + + private static class OkHttpInspectorRequest implements NetworkEventReporter.InspectorRequest { + private final String mRequestId; + private final Object /*Request*/ mRequest; + private RequestBodyHelper mRequestBodyHelper; + private final OkHttp3Helper mHelper; + + public OkHttpInspectorRequest( + OkHttp3Helper helper, + String requestId, + Object /*Request*/ request, + RequestBodyHelper requestBodyHelper) { + mHelper = helper; + mRequestId = requestId; + mRequest = request; + mRequestBodyHelper = requestBodyHelper; + } + + @Override + public String id() { + return mRequestId; + } + + @Override + public String friendlyName() { + // Hmm, can we do better? tag() perhaps? + return null; + } + + @Nullable + @Override + public Integer friendlyNameExtra() { + return null; + } + + @Override + public String url() { + return mHelper.Request_url(mRequest).toString(); + } + + @Override + public String method() { + return mHelper.Request_method(mRequest); + } + + @Nullable + @Override + public byte[] body() throws IOException { + Object /*RequestBody*/ body = mHelper.Request_body(mRequest); + if (body == null) { + return null; + } + OutputStream out = mRequestBodyHelper.createBodySink(firstHeaderValue("Content-Encoding")); + try (var bufferedSink = mHelper.Okio_buffer(mHelper.Okio_sink(out))) { + mHelper.RequestBody_writeTo(body, bufferedSink); + } + return mRequestBodyHelper.getDisplayBody(); + } + + @Override + public int headerCount() { + return mHelper.Headers_size(mHelper.Request_headers(mRequest)); + } + + @Override + public String headerName(int index) { + return mHelper.Headers_name(mHelper.Request_headers(mRequest), index); + } + + @Override + public String headerValue(int index) { + return mHelper.Headers_value(mHelper.Request_headers(mRequest), index); + } + + @Nullable + @Override + public String firstHeaderValue(String name) { + return mHelper.Request_header(mRequest, name); + } + } + + private static class OkHttpInspectorResponse implements NetworkEventReporter.InspectorResponse { + private final String mRequestId; + private final Object /*Request*/ mRequest; + private final Object /*Response*/ mResponse; + private @Nullable + final Object /*Connection*/ mConnection; + private final OkHttp3Helper mHelper; + + public OkHttpInspectorResponse( + OkHttp3Helper helper, + String requestId, + Object /*Request*/ request, + Object /*Response*/ response, + @Nullable Object /*Connection*/ connection) { + mHelper = helper; + mRequestId = requestId; + mRequest = request; + mResponse = response; + mConnection = connection; + } + + @Override + public String requestId() { + return mRequestId; + } + + @Override + public String url() { + return mHelper.Request_url(mRequest).toString(); + } + + @Override + public int statusCode() { + return mHelper.Response_code(mResponse); + } + + @Override + public String reasonPhrase() { + return mHelper.Response_message(mResponse); + } + + @Override + public boolean connectionReused() { + // Not sure... + return false; + } + + @Override + public int connectionId() { + return mConnection == null ? 0 : mConnection.hashCode(); + } + + @Override + public boolean fromDiskCache() { + return mHelper.Response_cacheResponse(mResponse) != null; + } + + @Override + public int headerCount() { + return mHelper.Headers_size(mHelper.Response_headers(mResponse)); + } + + @Override + public String headerName(int index) { + return mHelper.Headers_name(mHelper.Response_headers(mResponse), index); + } + + @Override + public String headerValue(int index) { + return mHelper.Headers_value(mHelper.Response_headers(mResponse), index); + } + + @Nullable + @Override + public String firstHeaderValue(String name) { + return mHelper.Response_header(mResponse, name); + } + } +} diff --git a/app/src/main/java/io/github/a13e300/tools/objects/HookFunction.java b/app/src/main/java/io/github/a13e300/tools/objects/HookFunction.java index 108c237..d4bced6 100644 --- a/app/src/main/java/io/github/a13e300/tools/objects/HookFunction.java +++ b/app/src/main/java/io/github/a13e300/tools/objects/HookFunction.java @@ -54,7 +54,7 @@ public String getClassName() { return "HookFunction"; } - private synchronized ClassLoader getClassLoader() { + synchronized ClassLoader getClassLoader() { if (mClassLoader == null) { var app = AndroidAppHelper.currentApplication(); if (app == null) return ClassLoader.getSystemClassLoader(); diff --git a/app/src/main/java/io/github/a13e300/tools/objects/OkHttpInterceptorObject.java b/app/src/main/java/io/github/a13e300/tools/objects/OkHttpInterceptorObject.java new file mode 100644 index 0000000..dcd90f8 --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/objects/OkHttpInterceptorObject.java @@ -0,0 +1,110 @@ +package io.github.a13e300.tools.objects; + +import com.facebook.stetho.rhino.JsConsole; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Wrapper; +import org.mozilla.javascript.annotations.JSFunction; + +import java.util.Set; + +import de.robv.android.xposed.XC_MethodHook; +import io.github.a13e300.tools.Logger; +import io.github.a13e300.tools.network.okhttp3.StethoOkHttp3ProxyInterceptor; + +public class OkHttpInterceptorObject extends ScriptableObject { + private Set unhook = null; + + public OkHttpInterceptorObject() {} + + public OkHttpInterceptorObject(Scriptable scope) { + setParentScope(scope); + Object ctor = ScriptRuntime.getTopLevelProp(scope, getClassName()); + if (ctor instanceof Scriptable) { + Scriptable scriptable = (Scriptable) ctor; + setPrototype((Scriptable) scriptable.get("prototype", scriptable)); + } + } + + @Override + public String getClassName() { + return "OkHttpInterceptorObject"; + } + + private static final int USE_DEOBF_AUTO = 0; + private static final int USE_DEOBF_YES = 1; + private static final int USE_DEOBF_NO = 2; + + public synchronized void start(ClassLoader classLoader, int useDeObf) { + if (unhook == null) { + var console = JsConsole.fromScope(getParentScope()); + if (useDeObf == USE_DEOBF_AUTO) { + try { + unhook = StethoOkHttp3ProxyInterceptor.start(classLoader, false); + } catch (Throwable t) { + Logger.e("Okhttp3Interceptor start with useDeObf=false failed, try useDeObf=true", t); + console.log("Okhttp3Interceptor start with useDeObf=false failed, try useDeObf=true"); + unhook = StethoOkHttp3ProxyInterceptor.start(classLoader, true); + } + } else { + unhook = StethoOkHttp3ProxyInterceptor.start(classLoader, useDeObf == USE_DEOBF_YES); + } + Logger.d("OkHttp3Interceptor started!"); + console.log("Okhttp3Interceptor started!"); + } else { + throw new IllegalStateException("Okhttp3Interceptor has already started!"); + } + } + + public synchronized void stop(boolean finalize) { + if (unhook != null) { + for (var h: unhook) { + h.unhook(); + } + unhook = null; + Logger.d("OkHttp3Interceptor stopped!"); + if (!finalize) { + JsConsole.fromScope(getParentScope()).log("Okhttp3Interceptor stopped!"); + } + } else if (!finalize) { + throw new IllegalStateException("Okhttp3Interceptor has not yet started!"); + } + } + + private static void usage() { + throw new IllegalArgumentException("usage: start([useObf] [, classLoader])"); + } + + @JSFunction + public static void start(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Throwable { + var obj = (OkHttpInterceptorObject) thisObj; + var hook = (HookFunction) ScriptableObject.getProperty(obj.getParentScope(), "hook"); + ClassLoader cl = null; + int useDeObf; + if (args.length == 0) { + cl = hook.getClassLoader(); + useDeObf = USE_DEOBF_AUTO; + } else { + if (!(args[0] instanceof Boolean)) usage(); + useDeObf = ((Boolean) args[0]) ? USE_DEOBF_YES : USE_DEOBF_NO; + if (args.length == 2) { + var c = args[1]; + if (c instanceof Wrapper) c = ((Wrapper) c).unwrap(); + if (c instanceof ClassLoader) cl = (ClassLoader) c; + else usage(); + } else usage(); + } + obj.start(cl, useDeObf); + } + + @JSFunction + public static void stop(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Throwable { + var obj = (OkHttpInterceptorObject) thisObj; + obj.stop(false); + } + +} diff --git a/build.gradle.kts b/build.gradle.kts index eb8ea2d..5f9b4d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,13 @@ buildscript { + val kotlin_version by extra("1.9.10") repositories { google() mavenCentral() } dependencies { classpath("com.android.tools.build:gradle:8.1.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } }