diff --git a/build.gradle b/build.gradle index 18f2fce7..07735ba7 100644 --- a/build.gradle +++ b/build.gradle @@ -82,6 +82,7 @@ dependencies { implementation files('libs/agent15.jar') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1" + implementation "io.arrow-kt:arrow-core:2.1.2" izpack 'org.codehaus.izpack:izpack-dist:5.1.3' diff --git a/src/main/java/core/packetproxy/DuplexFactory.java b/src/main/java/core/packetproxy/DuplexFactory.java index bb2c816e..20effc45 100644 --- a/src/main/java/core/packetproxy/DuplexFactory.java +++ b/src/main/java/core/packetproxy/DuplexFactory.java @@ -107,8 +107,12 @@ public byte[] onClientChunkReceived(byte[] data) throws Exception { decoded_data = mods.replaceOnRequest(decoded_data, server, client_packet); byte[] decoded_hash = CryptUtils.sha1(decoded_data); - byte[] intercepted_data = InterceptController.getInstance().received(decoded_data, server, + + // TODO: InterceptController の receivedBlocking() はブロッキング互換ラッパーであり暫定対応。 + // DuplexFactory を Kotlin 化する際は suspend fun received() に置き換えること。 + byte[] intercepted_data = InterceptController.getInstance().receivedBlocking(decoded_data, server, client_packet); + byte[] intercepted_hash = CryptUtils.sha1(intercepted_data); client_packet.setModifiedData(intercepted_data); if (intercepted_data.length > 0 && !Arrays.equals(decoded_hash, intercepted_hash)) { @@ -166,8 +170,12 @@ public byte[] onServerChunkReceived(byte[] data) throws Exception { decoded_data = mods.replaceOnResponse(decoded_data, server, server_packet); byte[] decoded_hash = CryptUtils.sha1(decoded_data); - byte[] intercepted_data = InterceptController.getInstance().received(decoded_data, server, + + // TODO: InterceptController の receivedBlocking() はブロッキング互換ラッパーであり暫定対応。 + // DuplexFactory を Kotlin 化する際は suspend fun received() に置き換えること。 + byte[] intercepted_data = InterceptController.getInstance().receivedBlocking(decoded_data, server, client_packet, server_packet); + byte[] intercepted_hash = CryptUtils.sha1(intercepted_data); server_packet.setModifiedData(intercepted_data); if (intercepted_data.length > 0 && !Arrays.equals(decoded_hash, intercepted_hash)) { diff --git a/src/main/java/core/packetproxy/controller/InterceptController.java b/src/main/java/core/packetproxy/controller/InterceptController.java deleted file mode 100644 index 57764623..00000000 --- a/src/main/java/core/packetproxy/controller/InterceptController.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2019 DeNA Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package packetproxy.controller; - -import packetproxy.model.InterceptModel; -import packetproxy.model.InterceptOptions; -import packetproxy.model.Packet; -import packetproxy.model.Server; - -public class InterceptController { - - private static InterceptController instance; - - public static InterceptController getInstance() throws Exception { - if (instance == null) { - - instance = new InterceptController(); - } - return instance; - } - - private boolean forward; - private boolean forward_multiple; - private byte[] intercepted_data; - private InterceptModel interceptModel; - private Object lock; - private Object thread_lock; - private ResendController resend_controller; - - private InterceptController() throws Exception { - this.forward = false; - this.forward_multiple = false; - this.interceptModel = InterceptModel.getInstance(); - this.lock = new Object(); - this.thread_lock = new Object(); - this.resend_controller = ResendController.getInstance(); - } - - public void enableInterceptMode() { - interceptModel.enableInterceptMode(); - } - - public void disableInterceptMode(byte[] data) { - synchronized (lock) { - forward = true; - forward_multiple = false; - intercepted_data = data; - interceptModel.disableInterceptMode(); - lock.notify(); - } - } - - public void forward(byte[] data) { - synchronized (lock) { - forward = true; - forward_multiple = false; - intercepted_data = data; - lock.notify(); - } - } - - public void forward_multiple(byte[] data) { - synchronized (lock) { - forward = true; - forward_multiple = true; - intercepted_data = data; - lock.notify(); - } - } - - public void drop() { - synchronized (lock) { - forward = false; - forward_multiple = false; - intercepted_data = null; - lock.notify(); - } - } - - public byte[] received(byte[] data, Server server, Packet client_packet) throws Exception { - return received(data, server, client_packet, null); - } - - public byte[] received(byte[] data, Server server, Packet client_packet, Packet server_packet) throws Exception { - Packet target_packet = server_packet == null ? client_packet : server_packet; - synchronized (thread_lock) { - - // Intercept=ON & InterceptOption=ON、かつ、指定されたルールにマッチした場合にのみIntercept - boolean is_target_packet = interceptModel.isInterceptEnabled(); - if (is_target_packet) { - - if (InterceptOptions.getInstance().isEnabled()) { - - if (server_packet == null) { - - is_target_packet = InterceptOptions.getInstance().interceptOnRequest(server, client_packet); - } else { - - is_target_packet = InterceptOptions.getInstance().interceptOnResponse(server, client_packet, - server_packet); - } - } - } - if (!is_target_packet) { - - return data; - } - - synchronized (lock) { - interceptModel.setData(data, client_packet, server_packet); - lock.wait(); - interceptModel.clearData(); - if (forward == false) { // drop - - return new byte[]{}; - } - if (forward_multiple == true) { - - // Forward x20(= original x 1 + copy x 19) - resend_controller.resend(target_packet.getOneShotPacket(intercepted_data), 19, true); - target_packet.setResend(); - } - return intercepted_data; - } - } - } -} diff --git a/src/main/kotlin/core/packetproxy/controller/InterceptController.kt b/src/main/kotlin/core/packetproxy/controller/InterceptController.kt new file mode 100644 index 00000000..d1d1f00d --- /dev/null +++ b/src/main/kotlin/core/packetproxy/controller/InterceptController.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2026 DeNA Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package packetproxy.controller + +import arrow.core.None +import arrow.core.Option +import arrow.core.Some +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import packetproxy.model.InterceptModel +import packetproxy.model.InterceptOptions +import packetproxy.model.Packet +import packetproxy.model.Server + +/** + * パケットのインターセプト(傍受・改ざん・廃棄)を制御する。 + * + * プロキシパイプライン側(received)でパケットを捕捉し、UI側(forward/drop)からの 操作決定を待機する。InterceptModel を介して UI に状態を通知し、 + * InterceptOptions のルールに基づいて対象パケットをフィルタリングする。 + * - suspend fun received(): 新プロキシ向け。コルーチンをサスペンドして UI 操作を待つ。 + * - receivedBlocking(): DuplexFactory.java 向けの暫定ブリッジ(Deprecated)。 + */ +class InterceptController +private constructor( + private val interceptModel: InterceptModel = InterceptModel.getInstance(), + private val resendController: ResendController = ResendController.getInstance(), +) { + companion object { + @Volatile private var instance: InterceptController? = null + + @JvmStatic + @Throws(Exception::class) + fun getInstance(): InterceptController = + instance ?: synchronized(this) { instance ?: InterceptController().also { instance = it } } + } + + private sealed class InterceptDecision { + data class Forward(val data: ByteArray) : InterceptDecision() + + data class ForwardMultiple(val data: ByteArray) : InterceptDecision() + + data object Drop : InterceptDecision() + } + + private val mutex = Mutex() + private var pendingDeferred: CompletableDeferred? = null + + fun enableInterceptMode() { + interceptModel.enableInterceptMode() + } + + fun disableInterceptMode(data: ByteArray) { + pendingDeferred?.complete(InterceptDecision.Forward(data)) + interceptModel.disableInterceptMode() + } + + fun forward(data: ByteArray) { + pendingDeferred?.complete(InterceptDecision.Forward(data)) + } + + @Suppress("FunctionName") + fun forward_multiple(data: ByteArray) { + pendingDeferred?.complete(InterceptDecision.ForwardMultiple(data)) + } + + fun drop() { + pendingDeferred?.complete(InterceptDecision.Drop) + } + + /** + * suspend 版 + * + * 戻り値: + * - None = drop(ユーザーが意図した廃棄。エラーではない) + * - Some = forward(通過。データはユーザーが改ざんしている可能性あり) + */ + suspend fun received( + data: ByteArray, + server: Server?, + clientPacket: Packet, + serverPacket: Packet? = null, + ): Option { + val targetPacket = serverPacket ?: clientPacket + + if (!isInterceptTarget(server, clientPacket, serverPacket)) return Some(data) + + return mutex.withLock { + val deferred = CompletableDeferred() + pendingDeferred = deferred + interceptModel.setData(data, clientPacket, serverPacket) + try { + when (val decision = deferred.await()) { + is InterceptDecision.Drop -> None + is InterceptDecision.Forward -> Some(decision.data) + is InterceptDecision.ForwardMultiple -> { + resendController.resend(targetPacket.getOneShotPacket(decision.data), 19, true) + targetPacket.setResend() + Some(decision.data) + } + } + } finally { + interceptModel.clearData() + pendingDeferred = null + } + } + } + + /** + * Java 互換のブロッキングラッパー。DuplexFactory.java からの呼び出しのために存在する。 + * + * TODO: DuplexFactory が Kotlin 化され suspend fun received() を直接呼べるようになった時点で削除すること。 依存先: + * DuplexFactory.java(2箇所) + * - onClientChunkReceived() + * - onServerChunkReceived() + */ + @JvmOverloads + @Deprecated( + "Use suspend fun received() instead. Remove when DuplexFactory.java is migrated to Kotlin.", + level = DeprecationLevel.WARNING, + ) + fun receivedBlocking( + data: ByteArray, + server: Server?, + clientPacket: Packet, + serverPacket: Packet? = null, + ): ByteArray = runBlocking { + received(data, server, clientPacket, serverPacket).fold({ ByteArray(0) }) { it } + } + + private fun isInterceptTarget( + server: Server?, + clientPacket: Packet, + serverPacket: Packet?, + ): Boolean { + if (!interceptModel.isInterceptEnabled) return false + + if (InterceptOptions.getInstance().isEnabled) { + return if (serverPacket == null) { + InterceptOptions.getInstance().interceptOnRequest(server, clientPacket) + } else { + InterceptOptions.getInstance().interceptOnResponse(server, clientPacket, serverPacket) + } + } + + return true + } +}