Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ProxyManager): mass-importing proxies #4606

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
40d2ba2
wip(ProxyManager): import proxy modal (no file icon yet)
DataM0del Nov 22, 2024
29bb5ab
chore(integration/host.ts): enable `IN_DEV`
DataM0del Nov 22, 2024
40e82b1
wip(api/v1/proxies): add import routes
DataM0del Nov 22, 2024
5e4ff95
fix(ProxyFunctions/postImportClipboardProxy): add braces in guard cla…
DataM0del Nov 22, 2024
30d0616
fix(ProxyFunctions): rename `_requestObject` param to `requestObject`…
DataM0del Nov 22, 2024
e4dfb35
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 23, 2024
38947ea
add(theme/integration/rest): importProxyFromClipboard, importProxyFro…
DataM0del Nov 24, 2024
8501a71
Merge remote-tracking branch 'origin/feat/mass-proxy-importing' into …
DataM0del Nov 24, 2024
8f105d7
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 24, 2024
751af9e
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 25, 2024
07a1c90
refactor(Listenable): is running (#4685)
1zun4 Nov 25, 2024
9ef8a31
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 25, 2024
ca00f95
feat(FontRenderer): multi-atlas support (#4688)
superblaubeere27 Nov 25, 2024
7729501
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 25, 2024
96940e2
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 25, 2024
278e042
fix(importProxies): remove protocol from line
DataM0del Nov 25, 2024
55e96c1
feat(src-theme/routes/menu/proxymanager/ImportProxyModal): implement …
DataM0del Nov 25, 2024
6cd901e
fix(src-theme/routes/menu/proxymanager/ImportProxyModal): remove comm…
DataM0del Nov 25, 2024
e70c172
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 26, 2024
f8a2357
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 26, 2024
20a4b4e
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 27, 2024
b08db20
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Nov 30, 2024
e83ffa9
Merge branch 'nextgen' of https://github.com/CCBlueX/LiquidBounce int…
DataM0del Dec 3, 2024
5667b09
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Dec 21, 2024
93a7db2
Merge branch 'CCBlueX:nextgen' into feat/mass-proxy-importing
DataM0del Dec 23, 2024
f3477c9
refactor(ProxyFunctions): fix wrong path in comment
DataM0del Dec 25, 2024
5c2583f
draft(postImportFileProxy): use tinyfd
DataM0del Dec 25, 2024
5e4c92f
refactor(postImportFileProxy): implement
DataM0del Dec 26, 2024
fb6f9aa
refactor(ProxyValidator#Proxy/check): implement multiple ping servers
DataM0del Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src-theme/src/integration/host.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
13 changes: 13 additions & 0 deletions src-theme/src/integration/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions src-theme/src/routes/menu/proxymanager/ImportProxyModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- yes, I know, I pasted from the edit proxy modal, don't care. -->
<!-- probably will remove these comments later anyways... -->
<script lang="ts">
import IconTextButton from "../common/buttons/IconTextButton.svelte";
import Modal from "../common/modal/Modal.svelte";
import { importProxyFromFile, importProxyFromClipboard } from "../../../integration/rest";

export let visible: boolean;
</script>

<Modal title="Import Proxy" bind:visible={visible}>
<IconTextButton
title="From Clipboard" icon="icon-clipboard.svg"
on:click={() => importProxyFromClipboard() }/>
<IconTextButton
title="From File" icon="icon-file.svg"
on:click={() => importProxyFromFile() }/>
</Modal>
8 changes: 8 additions & 0 deletions src-theme/src/routes/menu/proxymanager/ProxyManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ProxyCheckResultEvent,
ProxyEditResultEvent
} from "../../../integration/events.js";
import ImportProxyModal from "./ImportProxyModal.svelte";

$: {
let filteredProxies = proxies;
Expand All @@ -51,6 +52,7 @@

let addProxyModalVisible = false;
let editProxyModalVisible = false;
let importProxyModalVisible = false;
let allCountries: string[] = [];

let searchQuery = "";
Expand Down Expand Up @@ -197,6 +199,7 @@
</script>

<AddProxyModal bind:visible={addProxyModalVisible}/>
<ImportProxyModal bind:visible={importProxyModalVisible} />
{#if currentEditProxy}
<EditProxyModal bind:visible={editProxyModalVisible} id={currentEditProxy.id}
host={currentEditProxy.host}
Expand Down Expand Up @@ -247,6 +250,11 @@
<ButtonContainer>
<IconTextButton icon="icon-plus-circle.svg" title="Add" on:click={() => addProxyModalVisible = true}/>
<IconTextButton icon="icon-clipboard.svg" title="Add Clipboard" on:click={() => addProxyFromClipboard()}/>
<IconTextButton
icon="icon-plus-circle.svg"
title="Import..."
on:click={() => importProxyModalVisible = true}
/>
<IconTextButton icon="icon-random.svg" disabled={renderedProxies.length === 0} title="Random"
on:click={connectToRandomProxy}/>
<IconTextButton icon="icon-disconnect.svg" disabled={!isConnectedToProxy} title="Disconnect"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@ import kotlin.jvm.optionals.getOrNull
/**
* This is a generic Minecraft server that is used to check if a proxy is working. The server also
* responds to query requests with the client's IP address.
* Some of these are taken from https://topminecraftservers.org/
*/
private const val PING_SERVER = "ping.liquidproxy.net"
private val FALLBACK_PING_SERVERS = listOf(
"mc.hypixel.net",
"crescentkingdom.com",
"gratopia.gg",
"terranova.mc.gg",
"play.arcanecloud.net",
"ping.liquidproxy.net"
)
private const val PING_TIMEOUT = 5

class ClientConnectionTicker(private val clientConnection: ClientConnection) : EventListener {
Expand All @@ -70,69 +78,75 @@ class ClientConnectionTicker(private val clientConnection: ClientConnection) : E
* as well as update the ip information of the proxy.
*/
fun Proxy.check(success: (Proxy) -> 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())
[email protected] = 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())
[email protected] = 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down