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. 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/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() 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 cf89d543..2f2e0b6a 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -26,11 +26,12 @@ 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.config.ProxyProperty.Companion.WORLDLIST_REFRESH_SECONDS 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 @@ -41,10 +42,11 @@ 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.target.ProxyTargetConfig.Companion.DEFAULT_NAME +import net.rsprox.proxy.target.ProxyTargetConfig.Companion.DEFAULT_VARP_COUNT 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,12 +58,14 @@ 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 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.* @@ -75,8 +79,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 +95,10 @@ public class ProxyService( private lateinit var credentials: BinaryCredentialsStore private val gamePackProvider: GamePackProvider = GamePackProvider() private var rspsModulus: String? = null + public lateinit var proxyTargets: List + private set + private val currentProxyTarget: ProxyTarget + get() = proxyTargets[getSelectedProxyTarget()] public fun start( rspsJavConfigUrl: String?, @@ -101,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) @@ -113,44 +119,88 @@ 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 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.65, "Proxy", "Reading binary credentials") - this.credentials = BinaryCredentialsStore.read() + progressCallback.update(0.10, "Proxy", "Loading RSProx (2/15)") + val proxyTargetConfigs = loadProxyTargetConfigs(rspsJavConfigUrl) + val jobs = mutableListOf>() + + 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.70, "Proxy", "Launching http server") - launchHttpServer(this.bootstrapFactory, worldListProvider, updatedJavConfig) - progressCallback.update(0.80, "Proxy", "Deleting temporary files") - deleteTemporaryClients() - deleteTemporaryRuneLiteJars() - progressCallback.update(0.90, "Proxy", "Transferring certificate") - transferFakeCertificate() - progressCallback.update(0.95, "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 { + val oldschool = + ProxyTargetConfig( + 0, + DEFAULT_NAME, + overriddenJavConfig ?: "http://oldschool.runescape.com/jav_config.ws", + ) + 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( + progressCallback: ProgressCallback, + configs: List, + ): List> { + this.proxyTargets = configs.map(::ProxyTarget) + return this.proxyTargets.map { target -> + createJob(progressCallback) { + target.load(properties, gamePackProvider, bootstrapFactory) + } + } } public fun updateCredentials( @@ -213,6 +263,15 @@ public class ProxyService( return properties.getPropertyOrNull(SELECTED_CLIENT) ?: 0 } + public fun getSelectedProxyTarget(): Int { + return properties.getPropertyOrNull(SELECTED_PROXY_TARGET) ?: 0 + } + + public fun setSelectedProxyTarget(index: Int) { + properties.setProperty(SELECTED_PROXY_TARGET, index) + properties.saveProperties(PROPERTIES_FILE) + } + public fun setAppSize( width: Int, height: Int, @@ -388,7 +447,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 @@ -427,8 +486,9 @@ public class ProxyService( character: JagexCharacter?, port: Int, ) { + val target = this.currentProxyTarget try { - launchProxyServer(this.bootstrapFactory, this.worldListProvider, rsa, port) + launchProxyServer(this.bootstrapFactory, target, rsa, port) } catch (t: Throwable) { logger.error(t) { "Unable to bind network port $port for native client." } return @@ -441,7 +501,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") @@ -449,15 +515,21 @@ 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) + } + if (target.config.name != DEFAULT_NAME) { + criteriaBuilder.name(target.config.name) + } + val criteria = criteriaBuilder.build() val result = patcher.patch( patched, @@ -467,12 +539,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})" @@ -480,6 +556,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, @@ -695,96 +782,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), @@ -804,6 +810,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/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/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/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 54369fba..1bf70bc7 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) @@ -73,7 +74,7 @@ public class ClientLoginInitializer( serverChannel, rsa, binaryWriteInterval, - worldListProvider, + target, decoderLoader, connections, filters, 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/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) } } 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/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 new file mode 100644 index 00000000..92d6e4d9 --- /dev/null +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTarget.kt @@ -0,0 +1,158 @@ +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 +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 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, + 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..c18400f5 --- /dev/null +++ b/proxy/src/main/kotlin/net/rsprox/proxy/target/ProxyTargetConfig.kt @@ -0,0 +1,37 @@ +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, + 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 + + 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, +) 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) {