From 5baabe1a527f77b48b50a97aad39fb58e1e63a62 Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 15:20:11 +0200 Subject: [PATCH 01/13] feat: boot rsprox up from dynamic proxy targets --- .../kotlin/net/rsprox/proxy/ProxyService.kt | 135 ++++------------ .../proxy/bootstrap/BootstrapFactory.kt | 5 +- .../proxy/client/ClientLoginInitializer.kt | 5 +- .../net/rsprox/proxy/target/ProxyTarget.kt | 148 ++++++++++++++++++ .../rsprox/proxy/target/ProxyTargetConfig.kt | 8 + .../proxy/worlds/DynamicWorldListProvider.kt | 6 +- .../rsprox/proxy/worlds/LocalHostAddress.kt | 9 +- .../kotlin/net/rsprox/proxy/worlds/World.kt | 5 +- .../net/rsprox/proxy/worlds/WorldList.kt | 25 ++- 9 files changed, 230 insertions(+), 116 deletions(-) create mode 100644 proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt create mode 100644 proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index cf89d543..410257a0 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -27,7 +27,6 @@ import net.rsprox.proxy.config.ProxyProperty.Companion.JAV_CONFIG_ENDPOINT import net.rsprox.proxy.config.ProxyProperty.Companion.PROXY_PORT_MIN import net.rsprox.proxy.config.ProxyProperty.Companion.SELECTED_CLIENT import net.rsprox.proxy.config.ProxyProperty.Companion.WORLDLIST_ENDPOINT -import net.rsprox.proxy.config.ProxyProperty.Companion.WORLDLIST_REFRESH_SECONDS import net.rsprox.proxy.connection.ClientTypeDictionary import net.rsprox.proxy.connection.ProxyConnectionContainer import net.rsprox.proxy.downloader.JagexNativeClientDownloader @@ -41,10 +40,9 @@ import net.rsprox.proxy.rsa.publicKey import net.rsprox.proxy.rsa.readOrGenerateRsaKey import net.rsprox.proxy.runelite.RuneliteLauncher import net.rsprox.proxy.settings.DefaultSettingSetStore +import net.rsprox.proxy.target.ProxyTarget +import net.rsprox.proxy.target.ProxyTargetConfig import net.rsprox.proxy.util.* -import net.rsprox.proxy.worlds.DynamicWorldListProvider -import net.rsprox.proxy.worlds.World -import net.rsprox.proxy.worlds.WorldListProvider import net.rsprox.shared.SessionMonitor import net.rsprox.shared.account.JagexAccountStore import net.rsprox.shared.account.JagexCharacter @@ -56,7 +54,6 @@ import org.newsclub.net.unix.AFUNIXSocketAddress import java.io.File import java.io.IOException import java.math.BigInteger -import java.net.URL import java.nio.file.Files import java.nio.file.LinkOption import java.nio.file.Path @@ -75,8 +72,6 @@ public class ProxyService( private val decoderLoader: DecoderLoader = DecoderLoader() private lateinit var bootstrapFactory: BootstrapFactory private lateinit var serverBootstrap: ServerBootstrap - private lateinit var httpServerBootstrap: ServerBootstrap - private lateinit var worldListProvider: WorldListProvider public lateinit var operatingSystem: OperatingSystem private set private lateinit var rsa: RSAPrivateCrtKeyParameters @@ -93,6 +88,8 @@ public class ProxyService( private lateinit var credentials: BinaryCredentialsStore private val gamePackProvider: GamePackProvider = GamePackProvider() private var rspsModulus: String? = null + private lateinit var proxyTargets: List + private var currentProxyTarget: ProxyTarget by Delegates.notNull() public fun start( rspsJavConfigUrl: String?, @@ -126,14 +123,11 @@ public class ProxyService( this.settingsStore = DefaultSettingSetStore.load(SETTINGS_DIRECTORY) this.availablePort = properties.getProperty(PROXY_PORT_MIN) this.bootstrapFactory = BootstrapFactory(allocator, properties) - progressCallback.update(0.35, "Proxy", "Loading jav config") - val javConfig = loadJavConfig(rspsJavConfigUrl) - progressCallback.update(0.40, "Proxy", "Loading world list") - this.worldListProvider = loadWorldListProvider(javConfig.getWorldListUrl()) - progressCallback.update(0.50, "Proxy", "Replacing codebase") - val replacementWorld = findCodebaseReplacementWorld(javConfig, worldListProvider) - progressCallback.update(0.60, "Proxy", "Rebuilding jav config") - val updatedJavConfig = rebuildJavConfig(javConfig, replacementWorld) + + progressCallback.update(0.35, "Proxy", "Loading proxy targets") + val proxyTargetConfigs = loadProxyTargetConfigs(rspsJavConfigUrl) + loadProxyTargets(proxyTargetConfigs) + progressCallback.update(0.65, "Proxy", "Reading binary credentials") this.credentials = BinaryCredentialsStore.read() @@ -142,8 +136,6 @@ public class ProxyService( if (operatingSystem == OperatingSystem.SOLARIS) { throw IllegalStateException("Operating system not supported for native: $operatingSystem") } - progressCallback.update(0.70, "Proxy", "Launching http server") - launchHttpServer(this.bootstrapFactory, worldListProvider, updatedJavConfig) progressCallback.update(0.80, "Proxy", "Deleting temporary files") deleteTemporaryClients() deleteTemporaryRuneLiteJars() @@ -153,6 +145,26 @@ public class ProxyService( setShutdownHook() } + private fun loadProxyTargetConfigs(overriddenJavConfig: String?): List { + val oldschool = + ProxyTargetConfig( + 0, + "OldSchool RuneScape", + overriddenJavConfig ?: "http://oldschool.runescape.com/jav_config.ws", + HTTP_SERVER_PORT, + ) + return listOf(oldschool) + } + + private fun loadProxyTargets(configs: List) { + this.proxyTargets = configs.map(::ProxyTarget) + for (target in this.proxyTargets) { + target.load(properties, gamePackProvider, bootstrapFactory) + } + // Currently assign it as first + this.currentProxyTarget = proxyTargets.first() + } + public fun updateCredentials( name: String, userId: Long, @@ -388,7 +400,7 @@ public class ProxyService( port: Int, ) { try { - launchProxyServer(this.bootstrapFactory, this.worldListProvider, rsa, port) + launchProxyServer(this.bootstrapFactory, this.currentProxyTarget, rsa, port) } catch (t: Throwable) { logger.error(t) { "Unable to bind network port $port for native client." } return @@ -428,7 +440,7 @@ public class ProxyService( port: Int, ) { try { - launchProxyServer(this.bootstrapFactory, this.worldListProvider, rsa, port) + launchProxyServer(this.bootstrapFactory, this.currentProxyTarget, rsa, port) } catch (t: Throwable) { logger.error(t) { "Unable to bind network port $port for native client." } return @@ -695,96 +707,15 @@ public class ProxyService( } } - private fun loadJavConfig(customUrl: String?): JavConfig { - val url = customUrl ?: "http://oldschool.runescape.com/jav_config.ws" - return runCatching("Failed to load jav_config.ws from $url") { - val config = JavConfig(URL(url)) - logger.debug { "Jav config loaded from $url" } - config - } - } - - private fun loadWorldListProvider(url: String): WorldListProvider { - return runCatching("Failed to instantiate world list provider") { - val provider = - DynamicWorldListProvider( - URL(url), - properties.getProperty(WORLDLIST_REFRESH_SECONDS), - ) - logger.debug { "World list provider loaded from $url" } - provider - } - } - - private fun findCodebaseReplacementWorld( - javConfig: JavConfig, - worldListProvider: WorldListProvider, - ): World { - val address = - javConfig - .getCodebase() - .removePrefix("http://") - .removePrefix("https://") - .removeSuffix("/") - return runCatching("Failed to find a linked world for codebase '$address'") { - val world = checkNotNull(worldListProvider.get().getTargetWorld(address)) - logger.debug { "Loaded initial world ${world.localHostAddress} <-> ${world.host}" } - world - } - } - - private fun rebuildJavConfig( - javConfig: JavConfig, - replacementWorld: World, - ): JavConfig { - return runCatching("Failed to rebuild jav_config.ws") { - val oldWorldList = javConfig.getWorldListUrl() - val oldCodebase = javConfig.getCodebase() - val changedWorldListUrl = "http://127.0.0.1:$HTTP_SERVER_PORT/worldlist.ws" - val changedCodebase = "http://${replacementWorld.localHostAddress}/" - val updated = - javConfig - .replaceWorldListUrl(changedWorldListUrl) - .replaceCodebase(changedCodebase) - logger.debug { "Rebuilt jav_config.ws:" } - logger.debug { "Codebase changed from '$oldCodebase' to '$changedCodebase'" } - logger.debug { "Worldlist changed from '$oldWorldList' to '$changedWorldListUrl'" } - updated - } - } - - private fun launchHttpServer( - factory: BootstrapFactory, - worldListProvider: WorldListProvider, - javConfig: JavConfig, - ) { - runCatching("Failure to launch HTTP server") { - val httpServerBootstrap = - factory.createWorldListHttpServer( - worldListProvider, - javConfig, - gamePackProvider, - ) - val timeoutSeconds = properties.getProperty(BIND_TIMEOUT_SECONDS).toLong() - httpServerBootstrap - .bind(HTTP_SERVER_PORT) - .asCompletableFuture() - .orTimeout(timeoutSeconds, TimeUnit.SECONDS) - .join() - this.httpServerBootstrap = httpServerBootstrap - logger.debug { "HTTP server bound to port $HTTP_SERVER_PORT" } - } - } - private fun launchProxyServer( factory: BootstrapFactory, - worldListProvider: WorldListProvider, + target: ProxyTarget, rsa: RSAPrivateCrtKeyParameters, port: Int, ) { val serverBootstrap = factory.createServerBootStrap( - worldListProvider, + target, rsa, decoderLoader, properties.getProperty(BINARY_WRITE_INTERVAL_SECONDS), diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/bootstrap/BootstrapFactory.kt b/proxy/src/main/kotlin/net/rsprox/proxy/bootstrap/BootstrapFactory.kt index a7a6db30..cfabea15 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/bootstrap/BootstrapFactory.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/bootstrap/BootstrapFactory.kt @@ -20,6 +20,7 @@ import net.rsprox.proxy.http.GamePackProvider import net.rsprox.proxy.http.HttpServerHandler import net.rsprox.proxy.plugin.DecoderLoader import net.rsprox.proxy.server.ServerConnectionInitializer +import net.rsprox.proxy.target.ProxyTarget import net.rsprox.proxy.worlds.WorldListProvider import net.rsprox.shared.filters.PropertyFilterSetStore import net.rsprox.shared.settings.SettingSetStore @@ -34,7 +35,7 @@ public class BootstrapFactory( } public fun createServerBootStrap( - worldListProvider: WorldListProvider, + target: ProxyTarget, rsa: RSAPrivateCrtKeyParameters, decoderLoader: DecoderLoader, binaryWriteInterval: Int, @@ -55,7 +56,7 @@ public class BootstrapFactory( .childHandler( ClientLoginInitializer( this, - worldListProvider, + target, rsa, decoderLoader, binaryWriteInterval, diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginInitializer.kt b/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginInitializer.kt index 54369fba..abddb5ba 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginInitializer.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginInitializer.kt @@ -15,9 +15,9 @@ import net.rsprox.proxy.client.prot.LoginClientProtProvider import net.rsprox.proxy.connection.ClientTypeDictionary import net.rsprox.proxy.connection.ProxyConnectionContainer import net.rsprox.proxy.plugin.DecoderLoader +import net.rsprox.proxy.target.ProxyTarget import net.rsprox.proxy.util.ChannelConnectionHandler import net.rsprox.proxy.worlds.LocalHostAddress -import net.rsprox.proxy.worlds.WorldListProvider import net.rsprox.shared.filters.PropertyFilterSetStore import net.rsprox.shared.settings.SettingSetStore import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters @@ -26,7 +26,7 @@ import java.net.InetSocketAddress public class ClientLoginInitializer( private val bootstrapFactory: BootstrapFactory, - private val worldListProvider: WorldListProvider, + private val target: ProxyTarget, private val rsa: RSAPrivateCrtKeyParameters, private val decoderLoader: DecoderLoader, private val binaryWriteInterval: Int, @@ -36,6 +36,7 @@ public class ClientLoginInitializer( ) : ChannelInitializer() { override fun initChannel(clientChannel: Channel) { val localHostAddress = getLocalHostAddress(clientChannel) + val worldListProvider = target.worldListProvider val worldList = worldListProvider.get() val world = worldList.getWorld(localHostAddress) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt new file mode 100644 index 00000000..9b9fe6bc --- /dev/null +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt @@ -0,0 +1,148 @@ +package net.rsprox.proxy.target + +import com.github.michaelbull.logging.InlineLogger +import io.netty.bootstrap.ServerBootstrap +import net.rsprox.proxy.bootstrap.BootstrapFactory +import net.rsprox.proxy.config.JavConfig +import net.rsprox.proxy.config.ProxyProperties +import net.rsprox.proxy.config.ProxyProperty +import net.rsprox.proxy.futures.asCompletableFuture +import net.rsprox.proxy.http.GamePackProvider +import net.rsprox.proxy.worlds.DynamicWorldListProvider +import net.rsprox.proxy.worlds.World +import net.rsprox.proxy.worlds.WorldListProvider +import java.net.URL +import java.util.concurrent.TimeUnit +import kotlin.system.exitProcess + +public class ProxyTarget( + public val config: ProxyTargetConfig, +) { + private val name: String + get() = config.name + private lateinit var httpServerBootstrap: ServerBootstrap + public lateinit var worldListProvider: WorldListProvider + private set + + public fun load( + properties: ProxyProperties, + gamePackProvider: GamePackProvider, + bootstrapFactory: BootstrapFactory, + ) { + val javConfig = loadJavConfig(config.javConfigUrl) + this.worldListProvider = loadWorldListProvider(properties, javConfig.getWorldListUrl()) + val replacementWorld = findCodebaseReplacementWorld(javConfig, worldListProvider) + val updatedJavConfig = rebuildJavConfig(javConfig, replacementWorld) + launchHttpServer( + properties, + bootstrapFactory, + worldListProvider, + updatedJavConfig, + gamePackProvider, + ) + } + + private fun loadJavConfig(url: String): JavConfig { + return runCatching("Failed to load jav_config.ws from $url for target '$name'") { + val config = JavConfig(URL(url)) + logger.debug { "Jav config loaded from $url for target '$name'" } + config + } + } + + private fun loadWorldListProvider( + properties: ProxyProperties, + url: String, + ): WorldListProvider { + return runCatching("Failed to instantiate world list provider for target '$name'") { + val provider = + DynamicWorldListProvider( + config, + URL(url), + properties.getProperty(ProxyProperty.WORLDLIST_REFRESH_SECONDS), + ) + logger.debug { "World list provider loaded from $url for target '$name'" } + provider + } + } + + private fun findCodebaseReplacementWorld( + javConfig: JavConfig, + worldListProvider: WorldListProvider, + ): World { + val address = + javConfig + .getCodebase() + .removePrefix("http://") + .removePrefix("https://") + .removeSuffix("/") + return runCatching("Failed to find a linked world for codebase '$address' for target '$name'") { + val world = checkNotNull(worldListProvider.get().getTargetWorld(address)) + logger.debug { "Loaded initial world ${world.localHostAddress} <-> ${world.host} for target '$name'" } + world + } + } + + private fun rebuildJavConfig( + javConfig: JavConfig, + replacementWorld: World, + ): JavConfig { + return runCatching("Failed to rebuild jav_config.ws for target '$name'") { + val oldWorldList = javConfig.getWorldListUrl() + val oldCodebase = javConfig.getCodebase() + val changedWorldListUrl = "http://127.0.0.1:${config.httpPort}/worldlist.ws" + val changedCodebase = "http://${replacementWorld.localHostAddress}/" + val updated = + javConfig + .replaceWorldListUrl(changedWorldListUrl) + .replaceCodebase(changedCodebase) + logger.debug { "Rebuilt jav_config.ws for target '$name':" } + logger.debug { "Codebase changed from '$oldCodebase' to '$changedCodebase'" } + logger.debug { "Worldlist changed from '$oldWorldList' to '$changedWorldListUrl'" } + updated + } + } + + private fun launchHttpServer( + properties: ProxyProperties, + factory: BootstrapFactory, + worldListProvider: WorldListProvider, + javConfig: JavConfig, + gamePackProvider: GamePackProvider, + ) { + runCatching("Failure to launch HTTP server for target '$name'") { + val httpServerBootstrap = + factory.createWorldListHttpServer( + worldListProvider, + javConfig, + gamePackProvider, + ) + val timeoutSeconds = properties.getProperty(ProxyProperty.BIND_TIMEOUT_SECONDS).toLong() + httpServerBootstrap + .bind(config.httpPort) + .asCompletableFuture() + .orTimeout(timeoutSeconds, TimeUnit.SECONDS) + .join() + this.httpServerBootstrap = httpServerBootstrap + logger.debug { "HTTP server bound to port ${config.httpPort} for target '$name'" } + } + } + + private inline fun runCatching( + errorMessage: String, + block: () -> T, + ): T { + try { + return block() + } catch (t: Throwable) { + logger.error(t) { + errorMessage + } + exitProcess(-1) + } + } + + private companion object { + private val logger = InlineLogger() + } +} diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt new file mode 100644 index 00000000..34f779ea --- /dev/null +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt @@ -0,0 +1,8 @@ +package net.rsprox.proxy.target + +public data class ProxyTargetConfig( + public val id: Int, + public val name: String, + public val javConfigUrl: String, + public val httpPort: Int, +) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/worlds/DynamicWorldListProvider.kt b/proxy/src/main/kotlin/net/rsprox/proxy/worlds/DynamicWorldListProvider.kt index dbf14f2b..c130a1e6 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/worlds/DynamicWorldListProvider.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/worlds/DynamicWorldListProvider.kt @@ -1,20 +1,22 @@ package net.rsprox.proxy.worlds +import net.rsprox.proxy.target.ProxyTargetConfig import java.net.URL import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeSource public class DynamicWorldListProvider( + private val proxyTargetConfig: ProxyTargetConfig, private val originalWorldListUrl: URL, private val cacheDurationSeconds: Int = 5, ) : WorldListProvider { - private var cached: WorldList = WorldList(originalWorldListUrl) + private var cached: WorldList = WorldList(proxyTargetConfig, originalWorldListUrl) private var lastUpdate: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() override fun get(): WorldList { if (lastUpdate.elapsedNow() > cacheDurationSeconds.seconds) { lastUpdate = TimeSource.Monotonic.markNow() - cached = WorldList(originalWorldListUrl) + cached = WorldList(proxyTargetConfig, originalWorldListUrl) } return cached } diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/worlds/LocalHostAddress.kt b/proxy/src/main/kotlin/net/rsprox/proxy/worlds/LocalHostAddress.kt index 813aec4c..6fafae1d 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/worlds/LocalHostAddress.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/worlds/LocalHostAddress.kt @@ -1,5 +1,7 @@ package net.rsprox.proxy.worlds +import net.rsprox.proxy.target.ProxyTargetConfig + @JvmInline public value class LocalHostAddress private constructor( public val ip: Int, @@ -20,7 +22,10 @@ public value class LocalHostAddress private constructor( private val ipv4Regex = Regex("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") - public fun fromWorldId(worldId: Int): LocalHostAddress { + public fun fromWorldId( + worldId: Int, + config: ProxyTargetConfig, + ): LocalHostAddress { require(worldId in 0..65535) { "World id out of bounds: $worldId" } @@ -31,7 +36,7 @@ public value class LocalHostAddress private constructor( LOCALHOST_GROUP_HEADER, b, c, - LOCALHOST_GROUP_SUFFIX, + LOCALHOST_GROUP_SUFFIX + config.id, ), ) } diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/worlds/World.kt b/proxy/src/main/kotlin/net/rsprox/proxy/worlds/World.kt index 3c775080..2a82b691 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/worlds/World.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/worlds/World.kt @@ -1,6 +1,9 @@ package net.rsprox.proxy.worlds +import net.rsprox.proxy.target.ProxyTargetConfig + public data class World( + public val proxyTargetConfig: ProxyTargetConfig, public val id: Int, public val properties: Int, public val population: Int, @@ -8,7 +11,7 @@ public data class World( public val host: String, public val activity: String, ) { - public val localHostAddress: LocalHostAddress = LocalHostAddress.fromWorldId(id) + public val localHostAddress: LocalHostAddress = LocalHostAddress.fromWorldId(id, proxyTargetConfig) public fun hasFlag(flag: WorldFlag): Boolean { return properties and flag.bitflag != 0 diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/worlds/WorldList.kt b/proxy/src/main/kotlin/net/rsprox/proxy/worlds/WorldList.kt index 55ff5155..20105859 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/worlds/WorldList.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/worlds/WorldList.kt @@ -4,13 +4,19 @@ import io.netty.buffer.ByteBufAllocator import io.netty.buffer.Unpooled import net.rsprot.buffer.JagByteBuf import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprox.proxy.target.ProxyTargetConfig import java.io.IOException import java.net.URL public data class WorldList( public val worlds: List, ) : List by worlds { - public constructor(url: URL) : this(parseWorlds(url)) + public constructor( + proxyTargetConfig: ProxyTargetConfig, + url: URL, + ) : this( + parseWorlds(proxyTargetConfig, url), + ) public fun encode(allocator: ByteBufAllocator): JagByteBuf { val capacity = estimateBufferCapacity() @@ -70,13 +76,22 @@ public data class WorldList( private companion object { @Throws(IOException::class) - private fun parseWorlds(url: URL): List { + private fun parseWorlds( + config: ProxyTargetConfig, + url: URL, + ): List { val bytes = url.readBytes() val buffer = Unpooled.wrappedBuffer(bytes).toJagByteBuf() - return decode(buffer) + return decode( + config, + buffer, + ) } - private fun decode(buffer: JagByteBuf): List { + private fun decode( + config: ProxyTargetConfig, + buffer: JagByteBuf, + ): List { val payloadSize = buffer.g4() val count = buffer.g2() val worldList = @@ -88,7 +103,7 @@ public data class WorldList( val activity = buffer.gjstr() val location = buffer.g1() val population = buffer.g2s() - add(World(id, properties, population, location, host, activity)) + add(World(config, id, properties, population, location, host, activity)) } } check(buffer.readerIndex() == payloadSize + Int.SIZE_BYTES) { From 74f9494130c5cae3ebcce997c9bbc1929b9cb024 Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 15:29:41 +0200 Subject: [PATCH 02/13] feat: add ui support for proxy targets dropdown --- .../net/rsprox/gui/components/LaunchBar.kt | 32 ++++++++++++++++++- .../kotlin/net/rsprox/proxy/ProxyService.kt | 11 ++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/components/LaunchBar.kt b/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/components/LaunchBar.kt index e51b751b..9cf7522e 100644 --- a/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/components/LaunchBar.kt +++ b/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/components/LaunchBar.kt @@ -8,6 +8,8 @@ import net.rsprox.gui.AppIcons import net.rsprox.gui.auth.JagexAuthenticator import net.rsprox.gui.sessions.SessionType import net.rsprox.gui.sessions.SessionsPanel +import net.rsprox.proxy.target.ProxyTarget +import net.rsprox.proxy.target.ProxyTargetConfig import net.rsprox.proxy.util.OperatingSystem import net.rsprox.shared.account.JagexCharacter import javax.swing.DefaultComboBoxModel @@ -45,7 +47,21 @@ public class LaunchBar( private val charactersModel = DefaultComboBoxModel() init { - layout = MigLayout("gap 10", "push[][][]", "[]") + layout = MigLayout("gap 10", "push[][][][]", "[]") + val targetConfigs = App.service.proxyTargets.map(ProxyTarget::config) + val targetConfigsModel = DefaultComboBoxModel(targetConfigs.toTypedArray()) + val proxyTargetDropdown = + FlatComboBox().apply { + model = targetConfigsModel + renderer = ProxyTargetCellRenderer() + selectedIndex = App.service.getSelectedProxyTarget() + } + proxyTargetDropdown.addActionListener { + App.service.setSelectedProxyTarget(proxyTargetDropdown.selectedIndex) + } + + proxyTargetDropdown.minimumWidth = 160 + add(proxyTargetDropdown, "growx") val characterDropdown = FlatComboBox() characterDropdown.model = charactersModel @@ -181,6 +197,20 @@ public class LaunchBar( } } + private class ProxyTargetCellRenderer : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean, + ) = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus).apply { + if (value is ProxyTargetConfig) { + text = value.name + } + } + } + private class JagexCharacterCellRenderer : DefaultListCellRenderer() { override fun getListCellRendererComponent( list: JList<*>?, diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index 410257a0..59697817 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -88,7 +88,8 @@ public class ProxyService( private lateinit var credentials: BinaryCredentialsStore private val gamePackProvider: GamePackProvider = GamePackProvider() private var rspsModulus: String? = null - private lateinit var proxyTargets: List + public lateinit var proxyTargets: List + private set private var currentProxyTarget: ProxyTarget by Delegates.notNull() public fun start( @@ -225,6 +226,14 @@ public class ProxyService( return properties.getPropertyOrNull(SELECTED_CLIENT) ?: 0 } + public fun getSelectedProxyTarget(): Int { + return proxyTargets.indexOf(currentProxyTarget) + } + + public fun setSelectedProxyTarget(index: Int) { + this.currentProxyTarget = proxyTargets[index] + } + public fun setAppSize( width: Int, height: Int, From 10089b07fb616ffba904ad7c4a167770fc4db208 Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 16:52:39 +0200 Subject: [PATCH 03/13] feat: initial support for connecting to multiple targets concurrently --- .../kotlin/net/rsprox/proxy/ProxyService.kt | 21 +++++++++++++------ .../rsprox/proxy/config/ProxyProperties.kt | 6 +++++- .../rsprox/proxy/target/ProxyTargetConfig.kt | 8 ++++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index 59697817..1b758c96 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -42,6 +42,7 @@ import net.rsprox.proxy.runelite.RuneliteLauncher import net.rsprox.proxy.settings.DefaultSettingSetStore import net.rsprox.proxy.target.ProxyTarget import net.rsprox.proxy.target.ProxyTargetConfig +import net.rsprox.proxy.target.ProxyTargetConfig.Companion.DEFAULT_VARP_COUNT import net.rsprox.proxy.util.* import net.rsprox.shared.SessionMonitor import net.rsprox.shared.account.JagexAccountStore @@ -448,8 +449,9 @@ public class ProxyService( character: JagexCharacter?, port: Int, ) { + val target = this.currentProxyTarget try { - launchProxyServer(this.bootstrapFactory, this.currentProxyTarget, rsa, port) + launchProxyServer(this.bootstrapFactory, target, rsa, port) } catch (t: Throwable) { logger.error(t) { "Unable to bind network port $port for native client." } return @@ -470,15 +472,18 @@ public class ProxyService( // For now, directly just download, patch and launch the C++ client val patcher = NativePatcher() - val criteria = + val criteriaBuilder = NativePatchCriteria .Builder(nativeClientType) .acceptAllLoopbackAddresses() .rsaModulus(rsa.publicKey.modulus.toString(16)) - .javConfig("http://127.0.0.1:$HTTP_SERVER_PORT/$javConfigEndpoint") - .worldList("http://127.0.0.1:$HTTP_SERVER_PORT/$worldlistEndpoint") + .javConfig("http://127.0.0.1:${target.config.httpPort}/$javConfigEndpoint") + .worldList("http://127.0.0.1:${target.config.httpPort}/$worldlistEndpoint") .port(port) - .build() + if (target.config.varpCount != DEFAULT_VARP_COUNT) { + criteriaBuilder.varpCount(DEFAULT_VARP_COUNT, target.config.varpCount) + } + val criteria = criteriaBuilder.build() val result = patcher.patch( patched, @@ -488,12 +493,16 @@ public class ProxyService( "Failed to patch" } checkNotNull(result.oldModulus) + val targetModulus = + target.config.modulus + ?: rspsModulus + ?: result.oldModulus registerConnection( ConnectionInfo( ClientType.Native, os, port, - BigInteger(rspsModulus ?: result.oldModulus, 16), + BigInteger(targetModulus, 16), ), ) ClientTypeDictionary[port] = "Native (${os.shortName})" diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/config/ProxyProperties.kt b/proxy/src/main/kotlin/net/rsprox/proxy/config/ProxyProperties.kt index 58daa233..4a4eef60 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/config/ProxyProperties.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/config/ProxyProperties.kt @@ -72,13 +72,17 @@ public value class ProxyProperties private constructor( private fun loadProperties(text: String): Properties { val properties = Properties(createDefaultProperties()) properties.load(text.byteInputStream(DEFAULT_PROPERTIES_CHARSET)) + // Migrate any 43601 to 43701 as we support multiple http servers now, which start at 43600 + if (properties.getValue(PROXY_PORT_MIN) == 43601) { + properties.setValue(PROXY_PORT_MIN, 43701) + } return properties } private fun createDefaultProperties(): Properties { val properties = Properties() // proxy - properties.setValue(PROXY_PORT_MIN, 43601) + properties.setValue(PROXY_PORT_MIN, 43701) properties.setValue(WORLDLIST_ENDPOINT, "worldlist.ws") properties.setValue(JAV_CONFIG_ENDPOINT, "javconfig.ws") properties.setValue(BIND_TIMEOUT_SECONDS, 30) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt index 34f779ea..147b0784 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt @@ -5,4 +5,10 @@ public data class ProxyTargetConfig( public val name: String, public val javConfigUrl: String, public val httpPort: Int, -) + public val modulus: String? = null, + public val varpCount: Int = DEFAULT_VARP_COUNT, +) { + public companion object { + public const val DEFAULT_VARP_COUNT: Int = 5000 + } +} From 7fdda014f068cf54a9ea94b629869ccdbee10d52 Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 17:02:18 +0200 Subject: [PATCH 04/13] feat: override native client's name when running as a custom target --- proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt | 6 +++++- .../kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index 1b758c96..c901e755 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -42,6 +42,7 @@ import net.rsprox.proxy.runelite.RuneliteLauncher import net.rsprox.proxy.settings.DefaultSettingSetStore import net.rsprox.proxy.target.ProxyTarget import net.rsprox.proxy.target.ProxyTargetConfig +import net.rsprox.proxy.target.ProxyTargetConfig.Companion.DEFAULT_NAME import net.rsprox.proxy.target.ProxyTargetConfig.Companion.DEFAULT_VARP_COUNT import net.rsprox.proxy.util.* import net.rsprox.shared.SessionMonitor @@ -151,7 +152,7 @@ public class ProxyService( val oldschool = ProxyTargetConfig( 0, - "OldSchool RuneScape", + DEFAULT_NAME, overriddenJavConfig ?: "http://oldschool.runescape.com/jav_config.ws", HTTP_SERVER_PORT, ) @@ -483,6 +484,9 @@ public class ProxyService( if (target.config.varpCount != DEFAULT_VARP_COUNT) { criteriaBuilder.varpCount(DEFAULT_VARP_COUNT, target.config.varpCount) } + if (target.config.name != DEFAULT_NAME) { + criteriaBuilder.name(target.config.name) + } val criteria = criteriaBuilder.build() val result = patcher.patch( diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt index 147b0784..e8296435 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt @@ -9,6 +9,7 @@ public data class ProxyTargetConfig( public val varpCount: Int = DEFAULT_VARP_COUNT, ) { public companion object { + public const val DEFAULT_NAME: String = "Old School RuneScape" public const val DEFAULT_VARP_COUNT: Int = 5000 } } From a6175a826b0b566d2db8bda8b2171c277deb1baa Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 17:35:02 +0200 Subject: [PATCH 05/13] fix: save last selected proxy target --- .../src/main/kotlin/net/rsprox/proxy/ProxyService.kt | 11 ++++++----- .../kotlin/net/rsprox/proxy/config/ProxyProperty.kt | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index c901e755..6aff02b5 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -26,6 +26,7 @@ import net.rsprox.proxy.config.ProxyProperty.Companion.FILTERS_STATUS import net.rsprox.proxy.config.ProxyProperty.Companion.JAV_CONFIG_ENDPOINT import net.rsprox.proxy.config.ProxyProperty.Companion.PROXY_PORT_MIN import net.rsprox.proxy.config.ProxyProperty.Companion.SELECTED_CLIENT +import net.rsprox.proxy.config.ProxyProperty.Companion.SELECTED_PROXY_TARGET import net.rsprox.proxy.config.ProxyProperty.Companion.WORLDLIST_ENDPOINT import net.rsprox.proxy.connection.ClientTypeDictionary import net.rsprox.proxy.connection.ProxyConnectionContainer @@ -92,7 +93,8 @@ public class ProxyService( private var rspsModulus: String? = null public lateinit var proxyTargets: List private set - private var currentProxyTarget: ProxyTarget by Delegates.notNull() + private val currentProxyTarget: ProxyTarget + get() = proxyTargets[getSelectedProxyTarget()] public fun start( rspsJavConfigUrl: String?, @@ -164,8 +166,6 @@ public class ProxyService( for (target in this.proxyTargets) { target.load(properties, gamePackProvider, bootstrapFactory) } - // Currently assign it as first - this.currentProxyTarget = proxyTargets.first() } public fun updateCredentials( @@ -229,11 +229,12 @@ public class ProxyService( } public fun getSelectedProxyTarget(): Int { - return proxyTargets.indexOf(currentProxyTarget) + return properties.getPropertyOrNull(SELECTED_PROXY_TARGET) ?: 0 } public fun setSelectedProxyTarget(index: Int) { - this.currentProxyTarget = proxyTargets[index] + properties.setProperty(SELECTED_PROXY_TARGET, index) + properties.saveProperties(PROPERTIES_FILE) } public fun setAppSize( diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/config/ProxyProperty.kt b/proxy/src/main/kotlin/net/rsprox/proxy/config/ProxyProperty.kt index fc3834cd..fb2ba004 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/config/ProxyProperty.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/config/ProxyProperty.kt @@ -23,5 +23,6 @@ public class ProxyProperty( val APP_POSITION_Y = ProxyProperty("app.position.y", IntProperty) val FILTERS_STATUS = ProxyProperty("filters.status", IntProperty) val SELECTED_CLIENT = ProxyProperty("app.client", IntProperty) + val SELECTED_PROXY_TARGET = ProxyProperty("app.target", IntProperty) } } From 2abccdb3c1576d747de7e99bbac016947365bf7d Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 20:18:21 +0200 Subject: [PATCH 06/13] feat: load proxy targets from a yaml file --- proxy/build.gradle.kts | 1 - .../kotlin/net/rsprox/proxy/ProxyService.kt | 11 ++++++++++- .../rsprox/proxy/target/ProxyTargetConfig.kt | 17 +++++++++++++++++ .../proxy/target/ProxyTargetConfigList.kt | 8 ++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfigList.kt diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 0d4ceecc..8416633c 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(projects.protocol.osrs226) implementation(projects.protocol.osrs227) implementation(projects.protocol.osrs228) - implementation(project(mapOf("path" to ":protocol:osrs-228"))) } tasks.build.configure { diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index 6aff02b5..604cfb6e 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -158,7 +158,15 @@ public class ProxyService( overriddenJavConfig ?: "http://oldschool.runescape.com/jav_config.ws", HTTP_SERVER_PORT, ) - return listOf(oldschool) + val customTargets = ProxyTargetConfig.load(PROXY_TARGETS_FILE) + val ids = customTargets.entries.map(ProxyTargetConfig::id).distinct() + check(ids.size == customTargets.entries.size) { + "Overlapping proxy target ids detected." + } + check(ids.all { it >= 1 }) { + "Proxy target ids must be >= 1" + } + return listOf(oldschool) + customTargets.entries } private fun loadProxyTargets(configs: List) { @@ -758,6 +766,7 @@ public class ProxyService( public companion object { private val logger = InlineLogger() + private val PROXY_TARGETS_FILE = CONFIGURATION_PATH.resolve("proxy-targets.yaml") private val PROPERTIES_FILE = CONFIGURATION_PATH.resolve("proxy.properties") private inline fun runCatching( diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt index e8296435..3946c648 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt @@ -1,15 +1,32 @@ package net.rsprox.proxy.target +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.readValue +import java.nio.file.Path +import kotlin.io.path.exists + public data class ProxyTargetConfig( public val id: Int, public val name: String, + @JsonProperty("jav_config_url") public val javConfigUrl: String, + @JsonProperty("http_port") public val httpPort: Int, public val modulus: String? = null, + @JsonProperty("varp_count") public val varpCount: Int = DEFAULT_VARP_COUNT, ) { public companion object { public const val DEFAULT_NAME: String = "Old School RuneScape" public const val DEFAULT_VARP_COUNT: Int = 5000 + + public fun load(path: Path): ProxyTargetConfigList { + if (!path.exists()) return ProxyTargetConfigList(emptyList()) + return ObjectMapper(YAMLFactory()) + .findAndRegisterModules() + .readValue(path.toFile()) + } } } diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfigList.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfigList.kt new file mode 100644 index 00000000..390aedd7 --- /dev/null +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfigList.kt @@ -0,0 +1,8 @@ +package net.rsprox.proxy.target + +import com.fasterxml.jackson.annotation.JsonProperty + +public data class ProxyTargetConfigList( + @JsonProperty("config") + public val entries: List, +) From 95d37237fc4aab82891cb4846f2525cfbd449fe6 Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 20:31:07 +0200 Subject: [PATCH 07/13] feat: make custom targets able to download historic natives --- .../kotlin/net/rsprox/proxy/ProxyService.kt | 20 ++++++++++++++++++- .../RuneWikiNativeClientDownloader.kt | 12 ++++++++++- .../rsprox/proxy/target/ProxyTargetConfig.kt | 1 + 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index 604cfb6e..7aede871 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -31,6 +31,7 @@ import net.rsprox.proxy.config.ProxyProperty.Companion.WORLDLIST_ENDPOINT import net.rsprox.proxy.connection.ClientTypeDictionary import net.rsprox.proxy.connection.ProxyConnectionContainer import net.rsprox.proxy.downloader.JagexNativeClientDownloader +import net.rsprox.proxy.downloader.RuneWikiNativeClientDownloader import net.rsprox.proxy.exceptions.MissingLibraryException import net.rsprox.proxy.filters.DefaultPropertyFilterSetStore import net.rsprox.proxy.futures.asCompletableFuture @@ -474,7 +475,13 @@ public class ProxyService( OperatingSystem.MAC -> NativeClientType.MAC else -> throw IllegalStateException() } - val binary = JagexNativeClientDownloader.download(nativeClientType) + val targetRev = target.config.revision + val binary = + if (targetRev == null) { + JagexNativeClientDownloader.download(nativeClientType) + } else { + getHistoricNativeClient(targetRev, nativeClientType) + } val extension = if (binary.extension.isNotEmpty()) ".${binary.extension}" else "" val stamp = System.currentTimeMillis() val patched = TEMP_CLIENTS_DIRECTORY.resolve("${binary.nameWithoutExtension}-$stamp$extension") @@ -523,6 +530,17 @@ public class ProxyService( launchExecutable(port, result.outputPath, os, character) } + private fun getHistoricNativeClient( + version: String, + type: NativeClientType, + ): Path { + return RuneWikiNativeClientDownloader.download( + CLIENTS_DIRECTORY, + type, + version, + ) + } + private fun launchJavaProcess( port: Int, operatingSystem: OperatingSystem, diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/downloader/RuneWikiNativeClientDownloader.kt b/proxy/src/main/kotlin/net/rsprox/proxy/downloader/RuneWikiNativeClientDownloader.kt index aa48c01c..032e8110 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/downloader/RuneWikiNativeClientDownloader.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/downloader/RuneWikiNativeClientDownloader.kt @@ -5,6 +5,7 @@ import net.rsprox.patch.NativeClientType import java.net.URL import java.nio.file.Files import java.nio.file.Path +import kotlin.io.path.exists import kotlin.io.path.writeBytes public object RuneWikiNativeClientDownloader { @@ -27,6 +28,16 @@ public object RuneWikiNativeClientDownloader { NativeClientType.WIN -> "osclient.exe" NativeClientType.MAC -> "osclient.app/Contents/MacOS/osclient" } + val filePathWithVersion = + when (type) { + NativeClientType.WIN -> "osclient-$version.exe" + NativeClientType.MAC -> "osclient.app/Contents/MacOS/osclient-$version" + } + val file = folder.resolve(filePathWithVersion) + // Return the old file if it already exists, assume it is unchanged + if (file.exists()) { + return file + } val url = URL(prefix + typePath + versionPath + filePath) val bytes = try { @@ -38,7 +49,6 @@ public object RuneWikiNativeClientDownloader { throw t } Files.createDirectories(folder) - val file = folder.resolve(filePath) file.writeBytes(bytes) return file } diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt index 3946c648..490355dc 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt @@ -17,6 +17,7 @@ public data class ProxyTargetConfig( public val modulus: String? = null, @JsonProperty("varp_count") public val varpCount: Int = DEFAULT_VARP_COUNT, + public val revision: String? = null, ) { public companion object { public const val DEFAULT_NAME: String = "Old School RuneScape" From 0ddaf0efbd09299152512feab79d92a6f2e5d1eb Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 20:41:41 +0200 Subject: [PATCH 08/13] feat: allow connecting to different revisions concurrently --- .../net/rsprox/proxy/binary/BinaryBlob.kt | 3 ++- .../rsprox/proxy/client/ClientLoginHandler.kt | 11 +++++----- .../proxy/client/ClientLoginInitializer.kt | 2 +- .../proxy/server/ServerGameLoginDecoder.kt | 20 +++++++++---------- .../net/rsprox/proxy/target/ProxyTarget.kt | 10 ++++++++++ 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/binary/BinaryBlob.kt b/proxy/src/main/kotlin/net/rsprox/proxy/binary/BinaryBlob.kt index a5c05022..0000f2dc 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/binary/BinaryBlob.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/binary/BinaryBlob.kt @@ -14,6 +14,7 @@ import net.rsprox.cache.resolver.LiveCacheResolver import net.rsprox.protocol.session.AttributeMap import net.rsprox.protocol.session.Session import net.rsprox.proxy.config.BINARY_PATH +import net.rsprox.proxy.config.CURRENT_REVISION import net.rsprox.proxy.plugin.DecoderLoader import net.rsprox.proxy.plugin.DecodingSession import net.rsprox.proxy.transcriber.LiveTranscriberSession @@ -201,7 +202,7 @@ public data class BinaryBlob( CacheProvider { OldSchoolCache(LiveCacheResolver(info), masterIndex) } - decoderLoader.load(provider, latestOnly = true) + decoderLoader.load(provider, latestOnly = header.revision == CURRENT_REVISION) val latestPlugin = decoderLoader.getDecoderOrNull(header.revision) if (latestPlugin == null) { logger.info { "Plugin for ${header.revision} missing, no live transcriber hooked." } diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginHandler.kt b/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginHandler.kt index 6865ecb8..0af91086 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginHandler.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginHandler.kt @@ -26,7 +26,6 @@ import net.rsprox.proxy.channel.replace import net.rsprox.proxy.client.prot.LoginClientProt import net.rsprox.proxy.client.util.HostPlatformStats import net.rsprox.proxy.client.util.LoginXteaBlock -import net.rsprox.proxy.config.CURRENT_REVISION import net.rsprox.proxy.config.getConnection import net.rsprox.proxy.connection.ProxyConnectionContainer import net.rsprox.proxy.js5.Js5MasterIndexArchive @@ -39,10 +38,10 @@ import net.rsprox.proxy.server.ServerJs5LoginHandler import net.rsprox.proxy.server.ServerRelayHandler import net.rsprox.proxy.server.prot.LoginServerProtId import net.rsprox.proxy.server.prot.LoginServerProtProvider +import net.rsprox.proxy.target.ProxyTarget import net.rsprox.proxy.util.ChannelConnectionHandler import net.rsprox.proxy.util.xteaEncrypt import net.rsprox.proxy.worlds.WorldFlag -import net.rsprox.proxy.worlds.WorldListProvider import net.rsprox.shared.filters.PropertyFilterSetStore import net.rsprox.shared.settings.SettingSetStore import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters @@ -51,7 +50,7 @@ public class ClientLoginHandler( private val serverChannel: Channel, private val rsa: RSAPrivateCrtKeyParameters, private val binaryWriteInterval: Int, - private val worldListProvider: WorldListProvider, + private val target: ProxyTarget, private val decoderLoader: DecoderLoader, private val connections: ProxyConnectionContainer, private val filters: PropertyFilterSetStore, @@ -122,8 +121,8 @@ public class ClientLoginHandler( val builder = ctx.channel().getBinaryHeaderBuilder() val buffer = msg.payload.toJagByteBuf() val version = buffer.g4() - if (version != CURRENT_REVISION) { - throw IllegalStateException("Out of date revision: $version") + if (version != target.revisionNum()) { + throw IllegalStateException("Invalid revision for target ${target.config.name}: $version") } val subVersion = buffer.g4() val clientType = buffer.g1() @@ -425,7 +424,7 @@ public class ClientLoginHandler( ServerGameLoginDecoder( ctx.channel(), binaryWriteInterval, - worldListProvider, + target, decoderLoader, connections, filters, diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginInitializer.kt b/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginInitializer.kt index abddb5ba..1bf70bc7 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginInitializer.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/client/ClientLoginInitializer.kt @@ -74,7 +74,7 @@ public class ClientLoginInitializer( serverChannel, rsa, binaryWriteInterval, - worldListProvider, + target, decoderLoader, connections, filters, diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/server/ServerGameLoginDecoder.kt b/proxy/src/main/kotlin/net/rsprox/proxy/server/ServerGameLoginDecoder.kt index 89d64913..ed61cc01 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/server/ServerGameLoginDecoder.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/server/ServerGameLoginDecoder.kt @@ -26,13 +26,12 @@ import net.rsprox.proxy.channel.replace import net.rsprox.proxy.client.ClientGameHandler import net.rsprox.proxy.client.ClientGenericDecoder import net.rsprox.proxy.client.ClientRelayHandler -import net.rsprox.proxy.config.CURRENT_REVISION import net.rsprox.proxy.config.LATEST_SUPPORTED_PLUGIN import net.rsprox.proxy.connection.ProxyConnectionContainer import net.rsprox.proxy.plugin.DecoderLoader import net.rsprox.proxy.server.prot.LoginServerProt +import net.rsprox.proxy.target.ProxyTarget import net.rsprox.proxy.util.UserUid -import net.rsprox.proxy.worlds.WorldListProvider import net.rsprox.shared.StreamDirection import net.rsprox.shared.filters.PropertyFilterSetStore import net.rsprox.shared.settings.SettingSetStore @@ -40,7 +39,7 @@ import net.rsprox.shared.settings.SettingSetStore public class ServerGameLoginDecoder( private val clientChannel: Channel, private val binaryWriteInterval: Int, - private val worldListProvider: WorldListProvider, + private val target: ProxyTarget, private val decoderLoader: DecoderLoader, private val connections: ProxyConnectionContainer, private val filters: PropertyFilterSetStore, @@ -268,7 +267,7 @@ public class ServerGameLoginDecoder( writeToClient { pdata(payload.copy()) } - val prot = decoderLoader.getDecoder(CURRENT_REVISION).gameServerProtProvider[0xFF] + val prot = decoderLoader.getDecoder(target.revisionNum()).gameServerProtProvider[0xFF] val packet = ServerPacket( prot, @@ -281,10 +280,10 @@ public class ServerGameLoginDecoder( pipeline.replace( ServerGenericDecoder( serverChannel.getServerToClientStreamCipher(), - decoderLoader.getDecoder(CURRENT_REVISION).gameServerProtProvider, + decoderLoader.getDecoder(target.revisionNum()).gameServerProtProvider, ), ) - pipeline.replace(ServerGameHandler(clientChannel, worldListProvider)) + pipeline.replace(ServerGameHandler(clientChannel, target.worldListProvider)) switchClientToGameDecoding(ctx) } if (state == State.LOGIN_OK_READ_DATA) { @@ -323,8 +322,7 @@ public class ServerGameLoginDecoder( sessionMonitor.onLogin(header) sessionMonitor.onUserInformationUpdate(userId, userHash) val blob = BinaryBlob(header, stream, binaryWriteInterval, sessionMonitor, filters, settings) - @Suppress("KotlinConstantConditions") - if (LATEST_SUPPORTED_PLUGIN >= CURRENT_REVISION) { + if (LATEST_SUPPORTED_PLUGIN >= target.revisionNum()) { blob.hookLiveTranscriber(key, decoderLoader) } val serverChannel = ctx.channel() @@ -354,10 +352,10 @@ public class ServerGameLoginDecoder( pipeline.replace( ServerGenericDecoder( serverChannel.getServerToClientStreamCipher(), - decoderLoader.getDecoder(CURRENT_REVISION).gameServerProtProvider, + decoderLoader.getDecoder(target.revisionNum()).gameServerProtProvider, ), ) - pipeline.replace(ServerGameHandler(clientChannel, worldListProvider)) + pipeline.replace(ServerGameHandler(clientChannel, target.worldListProvider)) switchClientToGameDecoding(ctx) } } @@ -367,7 +365,7 @@ public class ServerGameLoginDecoder( val clientPipeline = clientChannel.pipeline() clientPipeline.remove() clientPipeline.addLast( - ClientGenericDecoder(cipher, decoderLoader.getDecoder(CURRENT_REVISION).gameClientProtProvider), + ClientGenericDecoder(cipher, decoderLoader.getDecoder(target.revisionNum()).gameClientProtProvider), ) clientPipeline.addLast(ClientGameHandler(ctx.channel())) } diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt index 9b9fe6bc..92d6e4d9 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt @@ -3,6 +3,7 @@ package net.rsprox.proxy.target import com.github.michaelbull.logging.InlineLogger import io.netty.bootstrap.ServerBootstrap import net.rsprox.proxy.bootstrap.BootstrapFactory +import net.rsprox.proxy.config.CURRENT_REVISION import net.rsprox.proxy.config.JavConfig import net.rsprox.proxy.config.ProxyProperties import net.rsprox.proxy.config.ProxyProperty @@ -24,6 +25,15 @@ public class ProxyTarget( public lateinit var worldListProvider: WorldListProvider private set + public fun revisionNum(): Int { + // Improve this maybe? It's a little fragile like this + return config.revision + ?.split(".") + ?.firstOrNull() + ?.toIntOrNull() + ?: CURRENT_REVISION + } + public fun load( properties: ProxyProperties, gamePackProvider: GamePackProvider, From 7099820f15b39cec2be1b9416e508edf16552aad Mon Sep 17 00:00:00 2001 From: Kris Date: Sat, 25 Jan 2025 21:25:56 +0200 Subject: [PATCH 09/13] refactor: make custom target http port inferred from target id --- proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt | 1 - .../kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index 7aede871..a1cf8f74 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -157,7 +157,6 @@ public class ProxyService( 0, DEFAULT_NAME, overriddenJavConfig ?: "http://oldschool.runescape.com/jav_config.ws", - HTTP_SERVER_PORT, ) val customTargets = ProxyTargetConfig.load(PROXY_TARGETS_FILE) val ids = customTargets.entries.map(ProxyTargetConfig::id).distinct() diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt index 490355dc..c18400f5 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt @@ -1,24 +1,28 @@ package net.rsprox.proxy.target +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.readValue +import net.rsprox.proxy.config.HTTP_SERVER_PORT import java.nio.file.Path import kotlin.io.path.exists +@JsonIgnoreProperties(ignoreUnknown = true) public data class ProxyTargetConfig( public val id: Int, public val name: String, @JsonProperty("jav_config_url") public val javConfigUrl: String, - @JsonProperty("http_port") - public val httpPort: Int, public val modulus: String? = null, @JsonProperty("varp_count") public val varpCount: Int = DEFAULT_VARP_COUNT, public val revision: String? = null, ) { + public val httpPort: Int + get() = HTTP_SERVER_PORT + id + public companion object { public const val DEFAULT_NAME: String = "Old School RuneScape" public const val DEFAULT_VARP_COUNT: Int = 5000 From 10d20ae62fb0d26e5678323045d478e01fd7cbbf Mon Sep 17 00:00:00 2001 From: Kris Date: Sun, 26 Jan 2025 13:46:53 +0200 Subject: [PATCH 10/13] feat: add an error dialog when attempting to launch RL on a custom target --- .../net/rsprox/gui/dialogs/ErrorDialog.kt | 142 ++++++++++++++++++ .../net/rsprox/gui/sessions/SessionPanel.kt | 13 +- 2 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 gui/proxy-tool/src/main/kotlin/net/rsprox/gui/dialogs/ErrorDialog.kt diff --git a/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/dialogs/ErrorDialog.kt b/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/dialogs/ErrorDialog.kt new file mode 100644 index 00000000..eb69cd69 --- /dev/null +++ b/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/dialogs/ErrorDialog.kt @@ -0,0 +1,142 @@ +package net.rsprox.gui.dialogs + +import com.github.michaelbull.logging.InlineLogger +import net.rsprox.gui.SplashScreen +import java.awt.BorderLayout +import java.awt.Color +import java.awt.Dimension +import java.awt.Font +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import java.awt.image.BufferedImage +import java.io.IOException +import javax.imageio.ImageIO +import javax.swing.* +import javax.swing.border.EmptyBorder + +@Suppress("SameParameterValue") +public class ErrorDialog private constructor( + title: String, + message: String, +) : JDialog() { + private val rightColumn = JPanel() + private val font = Font(Font.DIALOG, Font.PLAIN, 12) + + init { + try { + SplashScreen::class.java.getResourceAsStream("rsprox_128.png").use { stream -> + setIconImage(ImageIO.read(stream)) + } + } catch (e: IOException) { + logger.error(e) { + "Unable to load rsprox 128 image" + } + } + try { + SplashScreen::class.java.getResourceAsStream("rsprox_splash.png").use { stream -> + val logo: BufferedImage = ImageIO.read(stream) + val runelite = JLabel() + runelite.setIcon(ImageIcon(logo)) + runelite.setAlignmentX(CENTER_ALIGNMENT) + runelite.setBackground(DARK_GRAY_COLOR) + runelite.setOpaque(true) + rightColumn.add(runelite) + } + } catch (e: IOException) { + logger.error(e) { + "Unable to load rsprox splash image" + } + } + addWindowListener( + object : WindowAdapter() { + override fun windowClosing(e: WindowEvent) { + dispose() + } + }, + ) + setTitle(title) + layout = BorderLayout() + val pane = contentPane + pane.setBackground(DARKER_GRAY_COLOR) + val leftPane = JPanel() + leftPane.setBackground(DARKER_GRAY_COLOR) + leftPane.setLayout(BorderLayout()) + val titleComponent = JLabel("There was an error in RSProx") + titleComponent.setForeground(Color.WHITE) + titleComponent.setFont(font.deriveFont(16f)) + titleComponent.setBorder(EmptyBorder(10, 10, 10, 10)) + leftPane.add(titleComponent, BorderLayout.NORTH) + leftPane.preferredSize = Dimension(400, 200) + val textArea = JTextArea(message) + textArea.setFont(font) + textArea.setBackground(DARKER_GRAY_COLOR) + textArea.setForeground(Color.LIGHT_GRAY) + textArea.setLineWrap(true) + textArea.setWrapStyleWord(true) + textArea.setBorder(EmptyBorder(10, 10, 10, 10)) + textArea.isEditable = false + leftPane.add(textArea, BorderLayout.CENTER) + pane.add(leftPane, BorderLayout.CENTER) + rightColumn.setLayout(BoxLayout(rightColumn, BoxLayout.Y_AXIS)) + rightColumn.setBackground(DARK_GRAY_COLOR) + rightColumn.maximumSize = Dimension(200, Int.MAX_VALUE) + pane.add(rightColumn, BorderLayout.EAST) + } + + public fun open() { + addButton("Exit") { + dispose() + } + pack() + SplashScreen.stop() + setLocationRelativeTo(null) + isVisible = true + } + + private fun addButton( + message: String, + action: Runnable, + ): ErrorDialog { + val button = JButton(message) + button.addActionListener { action.run() } + button.setFont(font) + button.setBackground(DARK_GRAY_COLOR) + button.setForeground(Color.LIGHT_GRAY) + button.setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, DARK_GRAY_COLOR.brighter()), + EmptyBorder(4, 4, 4, 4), + ), + ) + button.setAlignmentX(CENTER_ALIGNMENT) + button.maximumSize = Dimension(Int.MAX_VALUE, Int.MAX_VALUE) + button.setFocusPainted(false) + button.addChangeListener { + if (button.model.isPressed) { + button.setBackground(DARKER_GRAY_COLOR) + } else if (button.model.isRollover) { + button.setBackground(DARK_GRAY_HOVER_COLOR) + } else { + button.setBackground(DARK_GRAY_COLOR) + } + } + rightColumn.add(button) + rightColumn.revalidate() + return this + } + + public companion object { + private val logger = InlineLogger() + private val DARKER_GRAY_COLOR = Color(30, 30, 30) + private val DARK_GRAY_COLOR = Color(40, 40, 40) + private val DARK_GRAY_HOVER_COLOR = Color(35, 35, 35) + + public fun show( + title: String, + text: String, + ) { + val dialog = ErrorDialog(title, text) + dialog.open() + } + } +} diff --git a/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/sessions/SessionPanel.kt b/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/sessions/SessionPanel.kt index d393fc34..ea553d13 100644 --- a/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/sessions/SessionPanel.kt +++ b/gui/proxy-tool/src/main/kotlin/net/rsprox/gui/sessions/SessionPanel.kt @@ -8,14 +8,11 @@ import com.formdev.flatlaf.util.ColorFunctions import com.github.michaelbull.logging.InlineLogger import net.rsprox.gui.App import net.rsprox.gui.AppIcons +import net.rsprox.gui.dialogs.ErrorDialog import net.rsprox.proxy.binary.BinaryHeader import net.rsprox.shared.SessionMonitor import net.rsprox.shared.account.JagexCharacter -import net.rsprox.shared.property.OmitFilteredPropertyTreeFormatter -import net.rsprox.shared.property.Property -import net.rsprox.shared.property.PropertyFormatterCollection -import net.rsprox.shared.property.RootProperty -import net.rsprox.shared.property.isExcluded +import net.rsprox.shared.property.* import net.rsprox.shared.property.regular.GroupProperty import net.rsprox.shared.property.regular.ListProperty import net.rsprox.shared.symbols.SymbolDictionaryProvider @@ -184,6 +181,12 @@ public class SessionPanel( val time = measureTime { try { + if (type == SessionType.RuneLite && App.service.getSelectedProxyTarget() != 0) { + return@submit ErrorDialog.show( + "Error launching RuneLite", + "RSProx is unable to launch on a custom target using RuneLite.", + ) + } portNumber = App.service.allocatePort() when (type) { SessionType.Java -> TODO() From 3cdb8dd512640a5dc347765fe6dfc9a8f08561f8 Mon Sep 17 00:00:00 2001 From: Kris Date: Sun, 26 Jan 2025 13:52:21 +0200 Subject: [PATCH 11/13] refactor: improve proxy service load screen --- .../kotlin/net/rsprox/proxy/ProxyService.kt | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index a1cf8f74..9bbfae94 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -130,11 +130,11 @@ public class ProxyService( this.availablePort = properties.getProperty(PROXY_PORT_MIN) this.bootstrapFactory = BootstrapFactory(allocator, properties) - progressCallback.update(0.35, "Proxy", "Loading proxy targets") + progressCallback.update(0.35, "Proxy", "Loading proxy target configs") val proxyTargetConfigs = loadProxyTargetConfigs(rspsJavConfigUrl) - loadProxyTargets(proxyTargetConfigs) + loadProxyTargets(progressCallback, proxyTargetConfigs) - progressCallback.update(0.65, "Proxy", "Reading binary credentials") + progressCallback.update(0.80, "Proxy", "Reading binary credentials") this.credentials = BinaryCredentialsStore.read() this.operatingSystem = getOperatingSystem() @@ -142,12 +142,12 @@ public class ProxyService( if (operatingSystem == OperatingSystem.SOLARIS) { throw IllegalStateException("Operating system not supported for native: $operatingSystem") } - progressCallback.update(0.80, "Proxy", "Deleting temporary files") + progressCallback.update(0.90, "Proxy", "Deleting temporary files") deleteTemporaryClients() deleteTemporaryRuneLiteJars() - progressCallback.update(0.90, "Proxy", "Transferring certificate") + progressCallback.update(0.95, "Proxy", "Transferring certificate") transferFakeCertificate() - progressCallback.update(0.95, "Proxy", "Setting up safe shutdown") + progressCallback.update(0.98, "Proxy", "Setting up safe shutdown") setShutdownHook() } @@ -169,10 +169,20 @@ public class ProxyService( return listOf(oldschool) + customTargets.entries } - private fun loadProxyTargets(configs: List) { + private fun loadProxyTargets( + progressCallback: ProgressCallback, + configs: List, + ) { this.proxyTargets = configs.map(::ProxyTarget) - for (target in this.proxyTargets) { + var percentage = 0.40 + for ((index, target) in this.proxyTargets.withIndex()) { + progressCallback.update( + percentage, + "Proxy", + "Loading proxy targets (${index.inc()}/${proxyTargets.size})", + ) target.load(properties, gamePackProvider, bootstrapFactory) + percentage += 0.50 / proxyTargets.size } } From 79f7bae90e152a2eee0988dda535c4d4b5b15038 Mon Sep 17 00:00:00 2001 From: Kris Date: Sun, 26 Jan 2025 14:05:44 +0200 Subject: [PATCH 12/13] refactor: multithread rsprox launch --- .../kotlin/net/rsprox/proxy/ProxyService.kt | 89 +++++++++++-------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index 9bbfae94..2f2e0b6a 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -62,7 +62,10 @@ import java.nio.file.Files import java.nio.file.LinkOption import java.nio.file.Path import java.util.* +import java.util.concurrent.Callable +import java.util.concurrent.ForkJoinPool import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger import java.util.stream.Collectors import kotlin.concurrent.thread import kotlin.io.path.* @@ -104,7 +107,7 @@ public class ProxyService( ) { this.rspsModulus = rspsModulus logger.info { "Starting proxy service" } - progressCallback.update(0.05, "Proxy", "Creating directories") + progressCallback.update(0.05, "Proxy", "Loading RSProx (1/15)") createConfigurationDirectories(CONFIGURATION_PATH) createConfigurationDirectories(BINARY_PATH) createConfigurationDirectories(CLIENTS_DIRECTORY) @@ -116,39 +119,58 @@ public class ProxyService( createConfigurationDirectories(SIGN_KEY_DIRECTORY) createConfigurationDirectories(BINARY_CREDENTIALS_FOLDER) createConfigurationDirectories(RUNELITE_LAUNCHER_REPO_DIRECTORY) - progressCallback.update(0.10, "Proxy", "Loading properties") - loadProperties() - progressCallback.update(0.15, "Proxy", "Loading Huffman") - HuffmanProvider.load() - progressCallback.update(0.20, "Proxy", "Loading RSA") - this.rsa = loadRsa() - progressCallback.update(0.25, "Proxy", "Loading jagex accounts") - this.jagexAccountStore = DefaultJagexAccountStore.load(JAGEX_ACCOUNTS_FILE) - progressCallback.update(0.30, "Proxy", "Loading property filters") - this.filterSetStore = DefaultPropertyFilterSetStore.load(FILTERS_DIRECTORY) - this.settingsStore = DefaultSettingSetStore.load(SETTINGS_DIRECTORY) - this.availablePort = properties.getProperty(PROXY_PORT_MIN) - this.bootstrapFactory = BootstrapFactory(allocator, properties) - - progressCallback.update(0.35, "Proxy", "Loading proxy target configs") + progressCallback.update(0.10, "Proxy", "Loading RSProx (2/15)") val proxyTargetConfigs = loadProxyTargetConfigs(rspsJavConfigUrl) - loadProxyTargets(progressCallback, proxyTargetConfigs) + val jobs = mutableListOf>() - progressCallback.update(0.80, "Proxy", "Reading binary credentials") - this.credentials = BinaryCredentialsStore.read() + jobs += + createJob(progressCallback) { + loadProperties() + this.availablePort = properties.getProperty(PROXY_PORT_MIN) + this.bootstrapFactory = BootstrapFactory(allocator, properties) + } + jobs += createJob(progressCallback) { HuffmanProvider.load() } + jobs += createJob(progressCallback) { this.rsa = loadRsa() } + jobs += + createJob(progressCallback) { this.jagexAccountStore = DefaultJagexAccountStore.load(JAGEX_ACCOUNTS_FILE) } + jobs += + createJob(progressCallback) { this.filterSetStore = DefaultPropertyFilterSetStore.load(FILTERS_DIRECTORY) } + jobs += createJob(progressCallback) { this.settingsStore = DefaultSettingSetStore.load(SETTINGS_DIRECTORY) } + + jobs += loadProxyTargets(progressCallback, proxyTargetConfigs) + + jobs += createJob(progressCallback) { this.credentials = BinaryCredentialsStore.read() } this.operatingSystem = getOperatingSystem() logger.debug { "Proxy launched on $operatingSystem" } if (operatingSystem == OperatingSystem.SOLARIS) { throw IllegalStateException("Operating system not supported for native: $operatingSystem") } - progressCallback.update(0.90, "Proxy", "Deleting temporary files") - deleteTemporaryClients() - deleteTemporaryRuneLiteJars() - progressCallback.update(0.95, "Proxy", "Transferring certificate") - transferFakeCertificate() - progressCallback.update(0.98, "Proxy", "Setting up safe shutdown") - setShutdownHook() + jobs += createJob(progressCallback) { deleteTemporaryClients() } + jobs += createJob(progressCallback) { deleteTemporaryRuneLiteJars() } + jobs += createJob(progressCallback) { transferFakeCertificate() } + jobs += createJob(progressCallback) { setShutdownHook() } + totalJobs.set(jobs.size + 2) + ForkJoinPool.commonPool().invokeAll(jobs) + } + + private val completedJobs = AtomicInteger(0) + private val totalJobs = AtomicInteger(0) + + private inline fun createJob( + progressCallback: ProgressCallback, + crossinline block: () -> Unit, + ): Callable { + return Callable { + block() + val num = completedJobs.incrementAndGet() + val percentage = num.toDouble() / totalJobs.get() + progressCallback.update( + 0.10 + percentage, + "Proxy", + "Loading RSProx ($num/${totalJobs.get()})", + ) + } } private fun loadProxyTargetConfigs(overriddenJavConfig: String?): List { @@ -172,17 +194,12 @@ public class ProxyService( private fun loadProxyTargets( progressCallback: ProgressCallback, configs: List, - ) { + ): List> { this.proxyTargets = configs.map(::ProxyTarget) - var percentage = 0.40 - for ((index, target) in this.proxyTargets.withIndex()) { - progressCallback.update( - percentage, - "Proxy", - "Loading proxy targets (${index.inc()}/${proxyTargets.size})", - ) - target.load(properties, gamePackProvider, bootstrapFactory) - percentage += 0.50 / proxyTargets.size + return this.proxyTargets.map { target -> + createJob(progressCallback) { + target.load(properties, gamePackProvider, bootstrapFactory) + } } } From be872e3aa551e9c1026629b4e55dc4033f6869a0 Mon Sep 17 00:00:00 2001 From: Kris Date: Sun, 26 Jan 2025 14:15:05 +0200 Subject: [PATCH 13/13] docs: add a section about custom proxy targets in readme --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 524ff113..4b18689c 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,46 @@ login, which is how third party clients tend to get caught, is unaffected. This was achieved via numerous clever tricks that will not be explored here, as to avoid people maliciously using them. +### Private Server Usage +RSProx can currently be used to connect to private servers, but only under +certain circumstances. The following criteria must be met in order to do this: + +> [!NOTE] +> This list is subject to changes over time, we hope to improve the overall +> support for further platforms and client types. + +1. This only works with Windows and Linux, not macOS. +2. RuneLite is not supported at this time. I will explore this possibility +after revision 229, when gamepacks are private. +3. The client must not have any protocol-breaking changes, same traditional +networking must be used. The only supported change at this time is changing +the varp count in the client from the size-5000 int array. +4. Must be on revision 223 or higher. + +#### Setting Up Custom Targets +In order to use the new proxy targets feature, one has to manually fill in the yaml file containing them. +The file is located at `user.home/.rsprox/proxy-targets.yaml` + +Here is an example RSPS target: +```yaml +config: + - id: 1 + name: Blurite + jav_config_url: "https://client.blurite.io/jav_local_227.ws" + varp_count: 15000 + revision: 227.3 + modulus: d2a780dccbcf534dc61a36deff725aabf9f46fc9ea298ac8c39b89b5bcb5d0817f8c9f59621187d448da9949aca848d0b2acae50c3122b7da53a79e6fe87ff76b675bcbf5bc18fbd2c9ed8f4cff2b7140508049eb119259af888eb9d20e8cea8a4384b06589483bcda11affd8d67756bc93a4d786494cdf7b634e3228b64116d +``` + +Properties breakdown: +`id` - A number from 1 to 100, must be unique. This is a required property. +`name` - The name given to the client. Any references to `OldSchool RuneScape` will be replaced by this. This is a required property to ensure caches don't overwrite and cause crashing at runtime when loading different games simultaneously. +`jav_config_url` - The URL to the jav_config that will be used to load initial world and world list. This is a required property. +`varp_count` - Changes the array length used for varps in the client, the default value is 5000. This is an optional property. +`revision` - A revision number used to pick the client and correct decoders. The default is whatever is currently latest stable in Old School RuneScape. This is an optional property. +`modulus` - A hexadecimal (base-16) RSA modulus used to encrypt the login packet sent to the client. This is a required property. + + ## Progress Below is a small task list showing a rough breakdown of what the tool will consist of, and how far the progress is at any given moment.