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 {