diff --git a/src-theme/src/integration/host.ts b/src-theme/src/integration/host.ts index 9124eed4f8a..9729cf99eb1 100644 --- a/src-theme/src/integration/host.ts +++ b/src-theme/src/integration/host.ts @@ -1,4 +1,4 @@ -const IN_DEV = false; +const IN_DEV = true; const DEV_PORT = 15000; export const REST_BASE = IN_DEV ? `http://localhost:${DEV_PORT}` : window.location.origin; diff --git a/src-theme/src/integration/rest.ts b/src-theme/src/integration/rest.ts index 18d4583ef0f..9b55eef20fa 100644 --- a/src-theme/src/integration/rest.ts +++ b/src-theme/src/integration/rest.ts @@ -490,6 +490,19 @@ export async function addProxyFromClipboard() { }); } +export async function importProxyFromClipboard() { + await fetch(`${API_BASE}/client/proxies/import/clipboard`, { + method: "POST" + }); +} + +export async function importProxyFromFile() { + await fetch(`${API_BASE}/client/proxies/import/file`, { + method: "POST" + }); +} + + export async function removeProxy(id: number) { await fetch(`${API_BASE}/client/proxies/remove`, { method: "DELETE", diff --git a/src-theme/src/routes/menu/proxymanager/ImportProxyModal.svelte b/src-theme/src/routes/menu/proxymanager/ImportProxyModal.svelte new file mode 100644 index 00000000000..808c27fe6e8 --- /dev/null +++ b/src-theme/src/routes/menu/proxymanager/ImportProxyModal.svelte @@ -0,0 +1,18 @@ + + + + + + importProxyFromClipboard() }/> + importProxyFromFile() }/> + diff --git a/src-theme/src/routes/menu/proxymanager/ProxyManager.svelte b/src-theme/src/routes/menu/proxymanager/ProxyManager.svelte index abae757dc68..c35cf14835c 100644 --- a/src-theme/src/routes/menu/proxymanager/ProxyManager.svelte +++ b/src-theme/src/routes/menu/proxymanager/ProxyManager.svelte @@ -34,6 +34,7 @@ ProxyCheckResultEvent, ProxyEditResultEvent } from "../../../integration/events.js"; + import ImportProxyModal from "./ImportProxyModal.svelte"; $: { let filteredProxies = proxies; @@ -51,6 +52,7 @@ let addProxyModalVisible = false; let editProxyModalVisible = false; + let importProxyModalVisible = false; let allCountries: string[] = []; let searchQuery = ""; @@ -197,6 +199,7 @@ + {#if currentEditProxy} addProxyModalVisible = true}/> addProxyFromClipboard()}/> + importProxyModalVisible = true} + /> Unit, failure: (Throwable) -> Unit) = runCatching { - logger.info("Request ping server via proxy... [$host:$port]") - - val serverAddress = ServerAddress.parse(PING_SERVER) - val socketAddress: InetSocketAddress = AllowedAddressResolver.DEFAULT.resolve(serverAddress) - .map(Address::getInetSocketAddress) - .getOrNull() - ?: error("Failed to resolve $PING_SERVER") - logger.info("Resolved ping server [$PING_SERVER]: $socketAddress") - - val clientConnection = ClientConnection(NetworkSide.CLIENTBOUND) - val channelFuture = connect(socketAddress, false, clientConnection) - channelFuture.syncUninterruptibly() - - val ticker = ClientConnectionTicker(clientConnection) + for (fallbackPingServer in FALLBACK_PING_SERVERS) { + logger.info("Request ping server via proxy... [$host:$port]") + + val serverAddress = ServerAddress.parse(fallbackPingServer) + val socketAddress: InetSocketAddress = AllowedAddressResolver.DEFAULT.resolve(serverAddress) + .map(Address::getInetSocketAddress) + .getOrNull() + ?: error("Failed to resolve $fallbackPingServer") + logger.info("Resolved server [$fallbackPingServer]: $socketAddress") + + val clientConnection = ClientConnection(NetworkSide.CLIENTBOUND) + val channelFuture = connect(socketAddress, false, clientConnection) + channelFuture.syncUninterruptibly() + + val ticker = ClientConnectionTicker(clientConnection) + + val clientQueryPacketListener = object : ClientQueryPacketListener { + + private var serverMetadata: ServerMetadata? = null + private var startTime = 0L + + override fun onResponse(packet: QueryResponseS2CPacket) { + if (serverMetadata != null) { + if (fallbackPingServer == FALLBACK_PING_SERVERS[FALLBACK_PING_SERVERS.lastIndex]) { + failure(IllegalStateException("Received multiple responses from server")) + } + return + } + + val metadata = packet.metadata() + serverMetadata = metadata + startTime = Util.getMeasuringTimeMs() + clientConnection.send(QueryPingC2SPacket(startTime)) + logger.info("Proxy Metadata [$host:$port]: ${metadata.description.convertToString()}") + } - val clientQueryPacketListener = object : ClientQueryPacketListener { + override fun onPingResult(packet: PingResultS2CPacket) { + val serverMetadata = this.serverMetadata ?: error("Received ping result without metadata") + val ping = Util.getMeasuringTimeMs() - startTime + logger.info("Proxy Ping [$host:$port]: $ping ms") - private var serverMetadata: ServerMetadata? = null - private var startTime = 0L + runCatching { + val ipInfo = IpInfoApi.someoneElse(serverMetadata.description.convertToString()) + this@check.ipInfo = ipInfo + logger.info("Proxy Info [$host:$port]: ${ipInfo.ip} [${ipInfo.country}, ${ipInfo.org}]") + }.onFailure { throwable -> + logger.error("Failed to update IP info for proxy [$host:$port]", throwable) + } - override fun onResponse(packet: QueryResponseS2CPacket) { - if (serverMetadata != null) { - failure(IllegalStateException("Received multiple responses from server")) - return + success(this@check) } - val metadata = packet.metadata() - serverMetadata = metadata - startTime = Util.getMeasuringTimeMs() - clientConnection.send(QueryPingC2SPacket(startTime)) - logger.info("Proxy Metadata [$host:$port]: ${metadata.description.convertToString()}") - } + override fun onDisconnected(info: DisconnectionInfo) { + EventManager.unregisterEventHandler(ticker) - override fun onPingResult(packet: PingResultS2CPacket) { - val serverMetadata = this.serverMetadata ?: error("Received ping result without metadata") - val ping = Util.getMeasuringTimeMs() - startTime - logger.info("Proxy Ping [$host:$port]: $ping ms") - - runCatching { - val ipInfo = IpInfoApi.someoneElse(serverMetadata.description.convertToString()) - this@check.ipInfo = ipInfo - logger.info("Proxy Info [$host:$port]: ${ipInfo.ip} [${ipInfo.country}, ${ipInfo.org}]") - }.onFailure { throwable -> - logger.error("Failed to update IP info for proxy [$host:$port]", throwable) + if (this.serverMetadata == null) { + if (fallbackPingServer == FALLBACK_PING_SERVERS[FALLBACK_PING_SERVERS.lastIndex]) { + failure(IllegalStateException("Disconnected before receiving metadata")) + } + } } - success(this@check) + override fun isConnectionOpen() = clientConnection.isOpen } - override fun onDisconnected(info: DisconnectionInfo) { - EventManager.unregisterEventHandler(ticker) - - if (this.serverMetadata == null) { - failure(IllegalStateException("Disconnected before receiving metadata")) - } - } - - override fun isConnectionOpen() = clientConnection.isOpen + clientConnection.connect(serverAddress.address, serverAddress.port, clientQueryPacketListener) + clientConnection.send(QueryRequestC2SPacket.INSTANCE) + logger.info("Sent query request via proxy [$host:$port]") } - - clientConnection.connect(serverAddress.address, serverAddress.port, clientQueryPacketListener) - clientConnection.send(QueryRequestC2SPacket.INSTANCE) - logger.info("Sent query request via proxy [$host:$port]") }.onFailure { throwable -> failure(throwable) } private fun Proxy.connect( diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/integration/interop/protocol/rest/v1/InteropFunctionRegistry.kt b/src/main/kotlin/net/ccbluex/liquidbounce/integration/interop/protocol/rest/v1/InteropFunctionRegistry.kt index 76f7fdf9f2d..2a0cdc0426e 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/integration/interop/protocol/rest/v1/InteropFunctionRegistry.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/integration/interop/protocol/rest/v1/InteropFunctionRegistry.kt @@ -93,6 +93,9 @@ internal fun registerInteropFunctions(node: Node) = node.withPath("/api/v1/clien get("/proxies", ::getProxies).apply { post("/add", ::postAddProxy) post("/clipboard", ::postClipboardProxy) + // Imports + post("/import/clipboard", ::postImportClipboardProxy) + post("/import/file", ::postImportFileProxy) post("/edit", ::postEditProxy) post("/check", ::postCheckProxy) delete("/remove", ::deleteRemoveProxy) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/integration/interop/protocol/rest/v1/client/ProxyFunctions.kt b/src/main/kotlin/net/ccbluex/liquidbounce/integration/interop/protocol/rest/v1/client/ProxyFunctions.kt index 66fcb04470d..87e14475c54 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/integration/interop/protocol/rest/v1/client/ProxyFunctions.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/integration/interop/protocol/rest/v1/client/ProxyFunctions.kt @@ -25,11 +25,15 @@ import com.mojang.blaze3d.systems.RenderSystem import io.netty.handler.codec.http.FullHttpResponse import net.ccbluex.liquidbounce.config.gson.interopGson import net.ccbluex.liquidbounce.features.misc.proxy.ProxyManager +import net.ccbluex.liquidbounce.utils.client.logger import net.ccbluex.liquidbounce.utils.client.mc import net.ccbluex.netty.http.model.RequestObject +import net.ccbluex.netty.http.util.httpBadRequest import net.ccbluex.netty.http.util.httpForbidden import net.ccbluex.netty.http.util.httpOk import org.lwjgl.glfw.GLFW +import org.lwjgl.util.tinyfd.TinyFileDialogs +import java.io.File /** * Proxy endpoints @@ -125,6 +129,78 @@ fun postClipboardProxy(requestObject: RequestObject): FullHttpResponse { return httpOk(JsonObject()) } +private fun importProxies(content: String) { + // TabNine moment + content.split("\n").map { line -> + var lineWithoutProtocol = line + if (lineWithoutProtocol.contains("://")) { + lineWithoutProtocol = line.split("://")[1] + } + val split = lineWithoutProtocol.split(":") + val host = split[0] + val port = split[1].toInt() + + if (split.size > 2) { + val username = split[2] + val password = split[3] + ProxyManager.addProxy(host, port, username, password, false) + } else { + ProxyManager.addProxy(host, port, "", "", false) + } + } +} + +// why are we overcomplicating things +// just have a get clipboard route or something... but ok fine, I'll do this anyway. +// POST /api/v1/client/proxies/import/clipboard +@Suppress("UNUSED_PARAMETER") +fun postImportClipboardProxy(requestObject: RequestObject): FullHttpResponse { + runCatching { + // Get clipboard content via GLFW + val clipboard = GLFW.glfwGetClipboardString(mc.window.handle)?: "" + logger.debug ("Get clipboard content via GLFW: $clipboard") + + if (!clipboard.isNotBlank()) { + logger.debug("Clipboard is empty, skip.") + return httpBadRequest("Clipboard is empty") + } + logger.debug("Clipboard content is not empty, import.") + + importProxies(clipboard) + } + + return httpOk(JsonObject()) +} + +// POST /api/v1/client/proxies/import/file +@Suppress("UNUSED_PARAMETER") +fun postImportFileProxy(requestObject: RequestObject): FullHttpResponse { + RenderSystem.recordRenderCall { + runCatching { + val result = TinyFileDialogs.tinyfd_openFileDialog( + "Select a proxy list", null, + null, null, true + ) + if (result == null || result.isEmpty()) { + logger.debug("No file selected, skip.") + return@runCatching httpBadRequest("No file selected") + } + + val paths = when (result.contains("|")) { + true -> result.split("|") + false -> listOf(result) + }.map { File(it) } + for ((index, file) in paths.withIndex()) { + logger.debug("Importing proxies from ${file.name} (#${index + 1})") + importProxies(file.readText()) + } +// val fileContent = File(result).readText() + } + } + + return httpOk(JsonObject()) +} + // POST /api/v1/client/proxies/edit @Suppress("UNUSED_PARAMETER") fun postEditProxy(requestObject: RequestObject): FullHttpResponse {