From 40ab1c8995ff2f624e4159891ac0648b816318c4 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Thu, 30 Jul 2026 06:29:05 +0800 Subject: [PATCH 01/15] feat(dropfile): add configurable limits for drop file --- .../li/cil/oc/api/internal/TextBuffer.java | 2 +- src/main/resources/application.conf | 7 +++ .../assets/opencomputers/lang/en_US.lang | 3 + src/main/scala/li/cil/oc/Localization.scala | 8 ++- src/main/scala/li/cil/oc/Settings.scala | 3 + .../scala/li/cil/oc/client/PacketSender.scala | 63 +++++++++++++------ .../oc/client/gui/traits/InputBuffer.scala | 42 ++++++++++--- .../scala/li/cil/oc/common/PacketFlags.scala | 9 +++ .../oc/common/component/GpuTextBuffer.scala | 2 +- .../cil/oc/common/component/TextBuffer.scala | 15 +++-- .../li/cil/oc/server/DropFileManager.scala | 55 ++++++++++++++++ .../li/cil/oc/server/DropFileSession.scala | 40 ++++++++++++ .../li/cil/oc/server/PacketHandler.scala | 43 +++++++------ 13 files changed, 236 insertions(+), 56 deletions(-) create mode 100644 src/main/scala/li/cil/oc/common/PacketFlags.scala create mode 100644 src/main/scala/li/cil/oc/server/DropFileManager.scala create mode 100644 src/main/scala/li/cil/oc/server/DropFileSession.scala diff --git a/src/main/java/li/cil/oc/api/internal/TextBuffer.java b/src/main/java/li/cil/oc/api/internal/TextBuffer.java index ae4a48f175..e80470f1be 100644 --- a/src/main/java/li/cil/oc/api/internal/TextBuffer.java +++ b/src/main/java/li/cil/oc/api/internal/TextBuffer.java @@ -590,7 +590,7 @@ public interface TextBuffer extends ManagedEnvironment, Persistable { * @param fileContent the content of the file being transferred. * @param player the player that dropped the file. Pass null on the client side. */ - void dropFile(String fileName, String fileContent, EntityPlayer player); + void dropFile(String fileName, byte[] fileContent, EntityPlayer player); /** * Signals a mouse button down event for the buffer. diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 074c3a4fc0..bc53c74c26 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1262,6 +1262,13 @@ opencomputers { # signal can contain multiple short lines. maxClipboard: 256 + # The maximum count of files that can be dropped at once. + maxDropFileCount: 16 + + # The maximum size in bytes for each dropped file. + # Note: Maximum value is 4MB. + maxDropFileSize: 65536 + # The TTL (Time-To-Live) upon creation of a network packet. When a packet # passes through a Relay, its TTL is decremented. If a Relay receives a # packet with a TTL of 0, the packet is dropped. Minimum value is 5. diff --git a/src/main/resources/assets/opencomputers/lang/en_US.lang b/src/main/resources/assets/opencomputers/lang/en_US.lang index e79850e4ce..d9087bf0ce 100644 --- a/src/main/resources/assets/opencomputers/lang/en_US.lang +++ b/src/main/resources/assets/opencomputers/lang/en_US.lang @@ -257,6 +257,9 @@ oc:gui.Switch.QueueSize=Queue size oc:gui.Switch.TransferRate=Cycle rate oc:gui.Terminal.InvalidKey=Invalid key, most likely another terminal has been bound to the server. oc:gui.Terminal.OutOfRange=No signal. +oc:gui.InputBuffer.TooMuchFiles=File count exceeds the maximum of %s. +oc:gui.InputBuffer.FileTooLarge=File size exceeds the maximum of %s bytes. +oc:gui.InputBuffer.FileNameTooLong=File name length exceeds the maximum of %s characters. # Containers oc:container.AccessPoint=Access Point diff --git a/src/main/scala/li/cil/oc/Localization.scala b/src/main/scala/li/cil/oc/Localization.scala index 23349438de..be66e13cba 100644 --- a/src/main/scala/li/cil/oc/Localization.scala +++ b/src/main/scala/li/cil/oc/Localization.scala @@ -165,6 +165,13 @@ object Localization { def OutOfRange = localizeLater("gui.Terminal.OutOfRange") } + object InputBuffer + { + def TooMuchFiles = localizeLater("gui.InputBuffer.TooMuchFiles", Settings.get.maxDropFileCount.toString) + def FileTooLarge = localizeLater("gui.InputBuffer.FileTooLarge", Settings.get.maxDropFileSize.toString) + def FileNameTooLong = localizeLater("gui.InputBuffer.FileNameTooLong", Settings.get.maxDropFileNameLength.toString) + } + object Tooltip { def DiskUsage(used: Long, capacity: Long) = localizeImmediately("tooltip.DiskUsage", used.toString, capacity.toString) @@ -184,5 +191,4 @@ object Localization { def MFULinked(isLinked: Boolean) = localizeImmediately(if (isLinked) "tooltip.UpgradeMF.Linked" else "tooltip.UpgradeMF.Unlinked") } - } diff --git a/src/main/scala/li/cil/oc/Settings.scala b/src/main/scala/li/cil/oc/Settings.scala index 1f4fa92da1..e5ec5e0308 100644 --- a/src/main/scala/li/cil/oc/Settings.scala +++ b/src/main/scala/li/cil/oc/Settings.scala @@ -344,6 +344,9 @@ class Settings(val config: Config) { val maxScreenHeight = config.getInt("misc.maxScreenHeight") max 1 val inputUsername = config.getBoolean("misc.inputUsername") val maxClipboard = config.getInt("misc.maxClipboard") max 1 + val maxDropFileCount = config.getInt("misc.maxDropFileCount") max 0 + val maxDropFileNameLength = 128 + val maxDropFileSize = config.getInt("misc.maxDropFileSize") min 4 * 1024 * 1024 val initialNetworkPacketTTL = config.getInt("misc.initialNetworkPacketTTL") max 5 val maxNetworkPacketSize = config.getInt("misc.maxNetworkPacketSize") max 0 // Need at least 4 for nanomachine protocol. Because I can! diff --git a/src/main/scala/li/cil/oc/client/PacketSender.scala b/src/main/scala/li/cil/oc/client/PacketSender.scala index 204d3f17da..d7c921eb8e 100644 --- a/src/main/scala/li/cil/oc/client/PacketSender.scala +++ b/src/main/scala/li/cil/oc/client/PacketSender.scala @@ -1,9 +1,8 @@ package li.cil.oc.client -import li.cil.oc.Settings -import li.cil.oc.common.CompressedPacketBuilder -import li.cil.oc.common.PacketType -import li.cil.oc.common.SimplePacketBuilder +import cpw.mods.fml.common.network.internal.FMLProxyPacket +import li.cil.oc.{Localization, Settings} +import li.cil.oc.common.{CompressedPacketBuilder, PacketFlags, PacketType, SimplePacketBuilder} import li.cil.oc.common.entity.Drone import li.cil.oc.common.tileentity._ import li.cil.oc.common.tileentity.traits.Computer @@ -13,6 +12,10 @@ import net.minecraft.item.ItemStack import net.minecraft.util.ResourceLocation import net.minecraftforge.common.util.ForgeDirection +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets +import java.util.zip.{Deflater, DeflaterOutputStream} + object PacketSender { // The server can queue this many clipboard chunks. Keep the client-side // whole-paste limit in line with that capacity instead of imposing the @@ -76,12 +79,16 @@ object PacketSender { pb.sendToServer() } + private def playErrorSound(): Unit = { + val player = Minecraft.getMinecraft.thePlayer + val handler = Minecraft.getMinecraft.getSoundHandler + handler.playSound(new PositionedSoundRecord(new ResourceLocation("note.harp"), 1, 1, player.posX.toFloat, player.posY.toFloat, player.posZ.toFloat)) + } + def sendClipboard(address: String, value: String) { if (value != null && !value.isEmpty) { if (value.length.toLong > maxClipboardLength || System.currentTimeMillis() < clipboardCooldown) { - val player = Minecraft.getMinecraft.thePlayer - val handler = Minecraft.getMinecraft.getSoundHandler - handler.playSound(new PositionedSoundRecord(new ResourceLocation("note.harp"), 1, 1, player.posX.toFloat, player.posY.toFloat, player.posZ.toFloat)) + playErrorSound() } else { clipboardCooldown = System.currentTimeMillis() + value.length / 10 @@ -97,21 +104,37 @@ object PacketSender { } } - def sendDropFile(address: String, name: String, content: String): Unit = { - val length = name.length + content.length - if (length > 64 * 1024) { - val player = Minecraft.getMinecraft.thePlayer - val handler = Minecraft.getMinecraft.getSoundHandler - handler.playSound(new PositionedSoundRecord(new ResourceLocation("note.harp"), 1, 1, player.posX.toFloat, player.posY.toFloat, player.posZ.toFloat)) + def sendDropFile(address: String, name: String, content: Array[Byte]): Unit = { + if (content.length > Settings.get.maxDropFileSize) { + playErrorSound() + Minecraft.getMinecraft.thePlayer.addChatMessage(Localization.InputBuffer.FileTooLarge) } else { - val pb = new CompressedPacketBuilder(PacketType.DropFile) - - pb.writeUTF(address) - pb.writeUTF(name) - pb.writeUTF(content) - - pb.sendToServer() + val data = new ByteArrayOutputStream() + val stream = new DeflaterOutputStream(data, new Deflater(Deflater.BEST_SPEED)) + stream.write(content) + stream.close() + // 1(compress) + 38(address) + 514(name) + 8(size) + 1(flag) + 8(size) = 570 + val chunks = data.toByteArray.grouped(31 * 1024).toArray + val size = chunks.map(_.length).sum + for (i <- chunks.indices) { + val chunk = chunks(i) + val pb = new SimplePacketBuilder(PacketType.DropFile) + val flag = PacketFlags.DropFile.Chunk | + (if (i == 0) PacketFlags.DropFile.Start else 0) | + (if (i == chunks.length - 1) PacketFlags.DropFile.End else 0) + pb.writeByte(flag) + if (i == 0) { + pb.writeUTF(address) + pb.writeUTF(name) + pb.writeInt(size) + } + pb.writeShort(chunk.length) + pb.write(chunk) + if (i == chunks.length - 1) + pb.writeInt(content.length) + pb.sendToServer() + } } } diff --git a/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala b/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala index aff35a5329..e49b996549 100644 --- a/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala +++ b/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala @@ -1,14 +1,16 @@ package li.cil.oc.client.gui.traits -import li.cil.oc.{OpenComputers, api} +import li.cil.oc.{Localization, OpenComputers, Settings, api} import li.cil.oc.client.{KeyBindings, Textures} import li.cil.oc.common.EventHandler import li.cil.oc.integration.util.NEI import li.cil.oc.util.RenderState import net.minecraft.client.Minecraft +import net.minecraft.client.audio.PositionedSoundRecord import net.minecraft.client.gui.GuiScreen import net.minecraft.client.gui.inventory.GuiContainer import net.minecraft.client.renderer.Tessellator +import net.minecraft.util.ResourceLocation import org.lwjgl.input.Keyboard import org.lwjgl.opengl.GL11 @@ -125,6 +127,7 @@ trait InputBuffer extends DisplayBuffer { } private val fileIoContext: ExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2)) + private case class FileResult(relativePath: String, file: File) private def getFiles(path: String): List[FileResult] = { @@ -145,18 +148,43 @@ trait InputBuffer extends DisplayBuffer { } } + private def playErrorSound(): Unit = { + val player = this.mc.thePlayer + val handler = this.mc.getSoundHandler + handler.playSound(new PositionedSoundRecord(new ResourceLocation("note.harp"), 1, 1, player.posX.toFloat, player.posY.toFloat, player.posZ.toFloat)) + } + def handleDropFile(filePath: String): Unit = { Future { - getFiles(filePath).foreach { - case FileResult(path, file) => - if (file.length() < 64 * 1024){ - val content = new String(Files.readAllBytes(file.toPath), StandardCharsets.UTF_8) + val allFiles = getFiles(filePath) + if (allFiles.size > Settings.get.maxDropFileCount) { + EventHandler.scheduleClient(() => { + this.mc.thePlayer.addChatMessage(Localization.InputBuffer.TooMuchFiles) + playErrorSound() + }) + } + else if (allFiles.exists(_.file.length() > Settings.get.maxDropFileSize)) { + EventHandler.scheduleClient(() => { + this.mc.thePlayer.addChatMessage(Localization.InputBuffer.FileTooLarge) + playErrorSound() + }) + } + else if (allFiles.exists(_.relativePath.length() > Settings.get.maxDropFileNameLength)) { + EventHandler.scheduleClient(() => { + this.mc.thePlayer.addChatMessage(Localization.InputBuffer.FileNameTooLong) + playErrorSound() + }) + } + else { + allFiles.foreach { + case FileResult(path, file) => + val content = Files.readAllBytes(file.toPath) EventHandler.scheduleClient(() => { buffer.dropFile(path, content, null) }) - } + } } - }(fileIoContext).failed.foreach{ e => + }(fileIoContext).failed.foreach { e => OpenComputers.log.warn("Failed to handle drop file.", e) }(fileIoContext) } diff --git a/src/main/scala/li/cil/oc/common/PacketFlags.scala b/src/main/scala/li/cil/oc/common/PacketFlags.scala new file mode 100644 index 0000000000..3f652bf3e6 --- /dev/null +++ b/src/main/scala/li/cil/oc/common/PacketFlags.scala @@ -0,0 +1,9 @@ +package li.cil.oc.common + +object PacketFlags { + object DropFile { + val Start = 1 << 0 + val Chunk = 1 << 1 + val End = 1 << 2 + } +} diff --git a/src/main/scala/li/cil/oc/common/component/GpuTextBuffer.scala b/src/main/scala/li/cil/oc/common/component/GpuTextBuffer.scala index 4e0d0887bd..ad63ea20b2 100644 --- a/src/main/scala/li/cil/oc/common/component/GpuTextBuffer.scala +++ b/src/main/scala/li/cil/oc/common/component/GpuTextBuffer.scala @@ -60,7 +60,7 @@ class GpuTextBuffer(val owner: String, val id: Int, val data: li.cil.oc.util.Tex override def keyDown(character: Char, code: Int, player: EntityPlayer): Unit = {} override def keyUp(character: Char, code: Int, player: EntityPlayer): Unit = {} override def clipboard(value: String, player: EntityPlayer): Unit = {} - override def dropFile(fileName: String, fileContent: String, player: EntityPlayer): Unit = {} + override def dropFile(fileName: String, fileContent: Array[Byte], player: EntityPlayer): Unit = {} override def mouseDown(x: Double, y: Double, button: Int, player: EntityPlayer): Unit = {} override def mouseDrag(x: Double, y: Double, button: Int, player: EntityPlayer): Unit = {} override def mouseUp(x: Double, y: Double, button: Int, player: EntityPlayer): Unit = {} diff --git a/src/main/scala/li/cil/oc/common/component/TextBuffer.scala b/src/main/scala/li/cil/oc/common/component/TextBuffer.scala index b380d0b5f5..40a9edec68 100644 --- a/src/main/scala/li/cil/oc/common/component/TextBuffer.scala +++ b/src/main/scala/li/cil/oc/common/component/TextBuffer.scala @@ -36,6 +36,7 @@ import net.minecraft.nbt.NBTTagCompound import net.minecraftforge.event.world.ChunkEvent import net.minecraftforge.event.world.WorldEvent +import java.nio.charset.StandardCharsets import scala.collection.convert.WrapAsJava._ import scala.collection.convert.WrapAsScala._ import scala.collection.mutable @@ -381,7 +382,7 @@ class TextBuffer(val host: EnvironmentHost) extends prefab.ManagedEnvironment wi override def clipboard(value: String, player: EntityPlayer): Unit = proxy.clipboard(value, player) - override def dropFile(fileName: String, fileContent: String, player: EntityPlayer): Unit = + override def dropFile(fileName: String, fileContent: Array[Byte], player: EntityPlayer): Unit = proxy.dropFile(fileName, fileContent, player) override def mouseDown(x: Double, y: Double, button: Int, player: EntityPlayer): Unit = @@ -603,7 +604,7 @@ object TextBuffer { def clipboard(value: String, player: EntityPlayer): Unit - def dropFile(fileName: String, fileContent: String, player: EntityPlayer): Unit + def dropFile(fileName: String, fileContent: Array[Byte], player: EntityPlayer): Unit def mouseDown(x: Double, y: Double, button: Int, player: EntityPlayer): Unit @@ -698,7 +699,7 @@ object TextBuffer { ClientPacketSender.sendClipboard(nodeAddress, value) } - override def dropFile(fileName: String, fileContent: String, player: EntityPlayer) { + override def dropFile(fileName: String, fileContent: Array[Byte], player: EntityPlayer) { debug(s"{type = dropFile}") ClientPacketSender.sendDropFile(nodeAddress, fileName, fileContent) } @@ -841,9 +842,11 @@ object TextBuffer { sendToKeyboards("keyboard.clipboard", player, value) } - override def dropFile(fileName: String, fileContent: String, player: EntityPlayer): Unit = { - if (owner.isUseableByPlayer(player)) - owner.node.sendToReachable("computer.checked_signal", player, "drop_file", fileName, fileContent) + override def dropFile(fileName: String, fileContent: Array[Byte], player: EntityPlayer): Unit = { + if (owner.isUseableByPlayer(player)) { + val content = new String(fileContent, StandardCharsets.UTF_8) + owner.node.sendToReachable("computer.checked_signal", player, "drop_file", fileName, content) + } } override def mouseDown(x: Double, y: Double, button: Int, player: EntityPlayer) { diff --git a/src/main/scala/li/cil/oc/server/DropFileManager.scala b/src/main/scala/li/cil/oc/server/DropFileManager.scala new file mode 100644 index 0000000000..f6caf562a2 --- /dev/null +++ b/src/main/scala/li/cil/oc/server/DropFileManager.scala @@ -0,0 +1,55 @@ +package li.cil.oc.server + +import com.google.common.cache.CacheBuilder +import li.cil.oc.{OpenComputers, Settings, api} +import net.minecraft.entity.player.EntityPlayer + +import java.util.UUID +import java.util.concurrent.TimeUnit + +object DropFileManager { + private val sessions = CacheBuilder.newBuilder() + .expireAfterAccess(8, TimeUnit.SECONDS) + .build[UUID, DropFileSession]() + + def onDropFileStart(address: String, fileName: String, compressedSize: Int, player: EntityPlayer): Unit = { + if (compressedSize > Settings.get.maxDropFileSize || compressedSize < 0) { + OpenComputers.log.warn(s"Rejected drop file from ${player.getCommandSenderName}: invalid compressed size $compressedSize.") + return + } + ComponentTracker.get(player.worldObj, address) match { + case Some(buffer: api.internal.TextBuffer) => + if (sessions.getIfPresent(player.getUniqueID) != null) + OpenComputers.log.warn(s"Player ${player.getCommandSenderName} started a new drop file before finishing the previous one. Overwriting.") + val session = new DropFileSession(fileName, compressedSize, player, buffer) + sessions.put(player.getUniqueID, session) + case _ => + OpenComputers.log.warn(s"Drop file target not found for address $address") + } + } + + def onDropFileChunk(data: Array[Byte], player: EntityPlayer): Unit = { + val session = sessions.getIfPresent(player.getUniqueID) + if (session != null) { + if (!session.onDropFileChunk(data)) + sessions.invalidate(player.getUniqueID) + } + else { + OpenComputers.log.warn(s"Received orphan drop file chunk from ${player.getCommandSenderName}.") + } + } + + def onDropFileEnd(unCompressedSize: Int, player: EntityPlayer): Unit = { + if (unCompressedSize > Settings.get.maxDropFileSize || unCompressedSize < 0) { + OpenComputers.log.warn(s"Rejected drop file from ${player.getCommandSenderName}: invalid uncompressed size $unCompressedSize.") + return + } + val session = sessions.getIfPresent(player.getUniqueID) + if (session != null) { + session.onDropFileEnd(unCompressedSize) + sessions.invalidate(player.getUniqueID) + } else { + OpenComputers.log.warn(s"Received orphan drop file end from ${player.getCommandSenderName}.") + } + } +} diff --git a/src/main/scala/li/cil/oc/server/DropFileSession.scala b/src/main/scala/li/cil/oc/server/DropFileSession.scala new file mode 100644 index 0000000000..4fb22325f2 --- /dev/null +++ b/src/main/scala/li/cil/oc/server/DropFileSession.scala @@ -0,0 +1,40 @@ +package li.cil.oc.server + +import li.cil.oc.{OpenComputers, api} +import net.minecraft.entity.player.EntityPlayer +import org.apache.commons.io.IOUtils + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream} +import java.util.zip.InflaterInputStream + +class DropFileSession(fileName: String, compressedSize: Int, player: EntityPlayer, target: api.internal.TextBuffer) { + private val compressed = new ByteArrayOutputStream(compressedSize) + + def onDropFileChunk(data: Array[Byte]): Boolean = { + if (compressed.size() + data.length <= compressedSize) { + compressed.write(data) + true + } else { + OpenComputers.log.warn(s"Receive a corrupt drop file packet from ${player.getCommandSenderName}: buffer overflow.") + false + } + } + + def onDropFileEnd(unCompressedSize: Int): Unit = { + if (compressed.size() != compressedSize) { + OpenComputers.log.warn(s"Incomplete drop file packet from ${player.getCommandSenderName}.") + return + } + val uncompressed = new InflaterInputStream(new ByteArrayInputStream(compressed.toByteArray)) + try { + val content = IOUtils.toByteArray(uncompressed) + if (content.length == unCompressedSize) + target.dropFile(fileName, content, player) + else + OpenComputers.log.warn(s"Receive a corrupt drop file packet from ${player.getCommandSenderName}. Decompressed size mismatch! Expected: $unCompressedSize, actually: ${content.length}.") + } finally { + uncompressed.close() + compressed.reset() + } + } +} diff --git a/src/main/scala/li/cil/oc/server/PacketHandler.scala b/src/main/scala/li/cil/oc/server/PacketHandler.scala index f363bbc29c..4df7979385 100644 --- a/src/main/scala/li/cil/oc/server/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/server/PacketHandler.scala @@ -2,25 +2,19 @@ package li.cil.oc.server import cpw.mods.fml.common.eventhandler.SubscribeEvent import cpw.mods.fml.common.network.FMLNetworkEvent.ServerCustomPacketEvent -import li.cil.oc.Localization -import li.cil.oc.OpenComputers -import li.cil.oc.api import li.cil.oc.api.internal.Server import li.cil.oc.api.machine.Machine -import li.cil.oc.common.Achievement -import li.cil.oc.common.PacketType import li.cil.oc.common.component.TextBuffer -import li.cil.oc.common.container import li.cil.oc.common.entity.Drone import li.cil.oc.common.item.Delegator import li.cil.oc.common.item.data.DriveData import li.cil.oc.common.item.traits.FileSystemLike import li.cil.oc.common.tileentity._ import li.cil.oc.common.tileentity.traits.Computer -import li.cil.oc.common.{PacketHandler => CommonPacketHandler} +import li.cil.oc.common.{Achievement, PacketFlags, PacketType, container, PacketHandler => CommonPacketHandler} import li.cil.oc.integration.fmp.EventHandler -import net.minecraft.entity.player.EntityPlayer -import net.minecraft.entity.player.EntityPlayerMP +import li.cil.oc.{Localization, OpenComputers, api} +import net.minecraft.entity.player.{EntityPlayer, EntityPlayerMP} import net.minecraft.nbt.NBTTagCompound import net.minecraft.network.NetHandlerPlayServer import net.minecraft.world.WorldServer @@ -106,7 +100,7 @@ object PacketHandler extends CommonPacketHandler { case Some(t) => t.getMountable(index) match { case server: Server => server - case _ => return // probably just lag, not invalid packet + case _ => return // probably just lag, not invalid packet } case _ => return } @@ -219,13 +213,22 @@ object PacketHandler extends CommonPacketHandler { } def onDropFile(p: PacketParser): Unit = { - val address = p.readUTF() - val fileName = p.readUTF() - val fileContent = p.readUTF() - if (fileName.length.toLong + fileContent.length > maxClientTextLength) return // Oversized; likely a forged client. - ComponentTracker.get(p.player.worldObj, address) match { - case Some(buffer: api.internal.TextBuffer) => buffer.dropFile(fileName, fileContent, p.player.asInstanceOf[EntityPlayer]) - case _ => // Invalid Packet + val flag = p.readByte() + if ((flag & PacketFlags.DropFile.Start) != 0) { + val address = p.readUTF() + val fileName = p.readUTF() + val size = p.readInt() + DropFileManager.onDropFileStart(address, fileName, size, p.player) + } + if ((flag & PacketFlags.DropFile.Chunk) != 0) { + val size = p.readUnsignedShort() + val content = new Array[Byte](size) + p.read(content) + DropFileManager.onDropFileChunk(content, p.player) + } + if ((flag & PacketFlags.DropFile.End) != 0) { + val size = p.readInt() + DropFileManager.onDropFileEnd(size, p.player) } } @@ -277,7 +280,7 @@ object PacketHandler extends CommonPacketHandler { val slot = p.readByte() val stack = p.readItemStack() p.player.openContainer match { - case db: container.Database => if (slot < db.rows*db.rows && slot >= 0) db.putStackInSlot(slot, stack) + case db: container.Database => if (slot < db.rows * db.rows && slot >= 0) db.putStackInSlot(slot, stack) case _ => // Invalid packet. } } @@ -313,7 +316,7 @@ object PacketHandler extends CommonPacketHandler { val side = p.readDirection() p.player match { case player: EntityPlayerMP => (player.openContainer, entity) match { - case (container: container.Rack, Some(readRack)) if readRack == container.rack => + case (container: container.Rack, Some(readRack)) if readRack == container.rack => if (container.rack.isUseableByPlayer(player)) container.rack.connect(mountableIndex, nodeIndex - 1, side) case _ => logForgedPacket(player) @@ -393,4 +396,4 @@ object PacketHandler extends CommonPacketHandler { case _ => // Invalid packet. } } -} \ No newline at end of file +} From 8910b3c76597100e106a9eb44685df0a9907a111 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:47:26 +0800 Subject: [PATCH 02/15] fix: limit uncompressed data read size to prevent OOM --- src/main/scala/li/cil/oc/server/DropFileSession.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/server/DropFileSession.scala b/src/main/scala/li/cil/oc/server/DropFileSession.scala index 4fb22325f2..0125e9835b 100644 --- a/src/main/scala/li/cil/oc/server/DropFileSession.scala +++ b/src/main/scala/li/cil/oc/server/DropFileSession.scala @@ -3,6 +3,7 @@ package li.cil.oc.server import li.cil.oc.{OpenComputers, api} import net.minecraft.entity.player.EntityPlayer import org.apache.commons.io.IOUtils +import org.apache.commons.io.input.BoundedInputStream import java.io.{ByteArrayInputStream, ByteArrayOutputStream} import java.util.zip.InflaterInputStream @@ -27,7 +28,7 @@ class DropFileSession(fileName: String, compressedSize: Int, player: EntityPlaye } val uncompressed = new InflaterInputStream(new ByteArrayInputStream(compressed.toByteArray)) try { - val content = IOUtils.toByteArray(uncompressed) + val content = IOUtils.toByteArray(new BoundedInputStream(uncompressed, unCompressedSize.toLong + 1)) if (content.length == unCompressedSize) target.dropFile(fileName, content, player) else From afd2e00a83f868985b179ce5c39a2ae9e7e2306f Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:47:48 +0800 Subject: [PATCH 03/15] fix: limit preallocate buffer size --- src/main/scala/li/cil/oc/server/DropFileSession.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/server/DropFileSession.scala b/src/main/scala/li/cil/oc/server/DropFileSession.scala index 0125e9835b..b1fa6b158b 100644 --- a/src/main/scala/li/cil/oc/server/DropFileSession.scala +++ b/src/main/scala/li/cil/oc/server/DropFileSession.scala @@ -9,7 +9,7 @@ import java.io.{ByteArrayInputStream, ByteArrayOutputStream} import java.util.zip.InflaterInputStream class DropFileSession(fileName: String, compressedSize: Int, player: EntityPlayer, target: api.internal.TextBuffer) { - private val compressed = new ByteArrayOutputStream(compressedSize) + private val compressed = new ByteArrayOutputStream(math.min(compressedSize, 32 * 1024)) def onDropFileChunk(data: Array[Byte]): Boolean = { if (compressed.size() + data.length <= compressedSize) { From 6891b5caa3105ee06ac85b01a7d7076dd9f60fa2 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:48:46 +0800 Subject: [PATCH 04/15] fix: prevent EntityPlayer leak --- .../scala/li/cil/oc/common/EventHandler.scala | 3 +- .../li/cil/oc/server/DropFileManager.scala | 9 +++- .../li/cil/oc/server/DropFileSession.scala | 44 +++++++++++++------ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/main/scala/li/cil/oc/common/EventHandler.scala b/src/main/scala/li/cil/oc/common/EventHandler.scala index 329476c56b..4d1a5cd259 100644 --- a/src/main/scala/li/cil/oc/common/EventHandler.scala +++ b/src/main/scala/li/cil/oc/common/EventHandler.scala @@ -31,7 +31,7 @@ import li.cil.oc.server.component.Keyboard import li.cil.oc.server.machine.Callbacks import li.cil.oc.server.machine.Machine import li.cil.oc.server.machine.luac.LuaStateFactory -import li.cil.oc.server.{PacketSender => ServerPacketSender} +import li.cil.oc.server.{DropFileManager, PacketSender => ServerPacketSender} import li.cil.oc.util.ExtendedWorld._ import li.cil.oc.util._ import net.minecraft.entity.player.EntityPlayer @@ -249,6 +249,7 @@ object EventHandler { @SubscribeEvent def onPlayerLogout(e: PlayerLoggedOutEvent) { keyboards.foreach(_.releasePressedKeys(e.player)) + DropFileManager.clearSession(e.player.getUniqueID) } @SubscribeEvent diff --git a/src/main/scala/li/cil/oc/server/DropFileManager.scala b/src/main/scala/li/cil/oc/server/DropFileManager.scala index f6caf562a2..61663c6b40 100644 --- a/src/main/scala/li/cil/oc/server/DropFileManager.scala +++ b/src/main/scala/li/cil/oc/server/DropFileManager.scala @@ -21,7 +21,7 @@ object DropFileManager { case Some(buffer: api.internal.TextBuffer) => if (sessions.getIfPresent(player.getUniqueID) != null) OpenComputers.log.warn(s"Player ${player.getCommandSenderName} started a new drop file before finishing the previous one. Overwriting.") - val session = new DropFileSession(fileName, compressedSize, player, buffer) + val session = new DropFileSession(fileName, compressedSize, player.getUniqueID, player.getCommandSenderName, buffer) sessions.put(player.getUniqueID, session) case _ => OpenComputers.log.warn(s"Drop file target not found for address $address") @@ -52,4 +52,11 @@ object DropFileManager { OpenComputers.log.warn(s"Received orphan drop file end from ${player.getCommandSenderName}.") } } + + def clearSession(playerUUID: UUID): Unit = { + val session = sessions.getIfPresent(playerUUID) + if (session != null) { + sessions.invalidate(playerUUID) + } + } } diff --git a/src/main/scala/li/cil/oc/server/DropFileSession.scala b/src/main/scala/li/cil/oc/server/DropFileSession.scala index b1fa6b158b..97096b8841 100644 --- a/src/main/scala/li/cil/oc/server/DropFileSession.scala +++ b/src/main/scala/li/cil/oc/server/DropFileSession.scala @@ -1,14 +1,17 @@ package li.cil.oc.server import li.cil.oc.{OpenComputers, api} -import net.minecraft.entity.player.EntityPlayer +import net.minecraft.entity.player.EntityPlayerMP +import net.minecraft.server.MinecraftServer import org.apache.commons.io.IOUtils import org.apache.commons.io.input.BoundedInputStream +import scala.collection.JavaConverters._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream} +import java.util.UUID import java.util.zip.InflaterInputStream -class DropFileSession(fileName: String, compressedSize: Int, player: EntityPlayer, target: api.internal.TextBuffer) { +class DropFileSession(fileName: String, compressedSize: Int, playerUUID: UUID, playerName: String, target: api.internal.TextBuffer) { private val compressed = new ByteArrayOutputStream(math.min(compressedSize, 32 * 1024)) def onDropFileChunk(data: Array[Byte]): Boolean = { @@ -16,26 +19,39 @@ class DropFileSession(fileName: String, compressedSize: Int, player: EntityPlaye compressed.write(data) true } else { - OpenComputers.log.warn(s"Receive a corrupt drop file packet from ${player.getCommandSenderName}: buffer overflow.") + OpenComputers.log.warn(s"Receive a corrupt drop file packet from $playerName:$playerUUID : buffer overflow.") false } } def onDropFileEnd(unCompressedSize: Int): Unit = { if (compressed.size() != compressedSize) { - OpenComputers.log.warn(s"Incomplete drop file packet from ${player.getCommandSenderName}.") + OpenComputers.log.warn(s"Incomplete drop file packet from $playerName:$playerUUID.") return } - val uncompressed = new InflaterInputStream(new ByteArrayInputStream(compressed.toByteArray)) - try { - val content = IOUtils.toByteArray(new BoundedInputStream(uncompressed, unCompressedSize.toLong + 1)) - if (content.length == unCompressedSize) - target.dropFile(fileName, content, player) - else - OpenComputers.log.warn(s"Receive a corrupt drop file packet from ${player.getCommandSenderName}. Decompressed size mismatch! Expected: $unCompressedSize, actually: ${content.length}.") - } finally { - uncompressed.close() - compressed.reset() + + val player = for { + server <- Option(MinecraftServer.getServer) + p <- server.getConfigurationManager.playerEntityList.asScala.collectFirst { + case p: EntityPlayerMP if p.getUniqueID == playerUUID => p + } + } yield p + + player match { + case Some (player) => + val uncompressed = new InflaterInputStream(new ByteArrayInputStream(compressed.toByteArray)) + try { + val content = IOUtils.toByteArray(new BoundedInputStream(uncompressed, unCompressedSize.toLong + 1)) + if (content.length == unCompressedSize) + target.dropFile(fileName, content, player) + else + OpenComputers.log.warn(s"Receive a corrupt drop file packet from $playerName:$playerUUID. Decompressed size mismatch! Expected: $unCompressedSize, actually: ${content.length}.") + } finally { + uncompressed.close() + compressed.reset() + } + case None => + OpenComputers.log.debug(s"Player $playerName:$playerUUID disconnected before drop file finished.") } } } From 498cd6be9a52fbb4a79f533c2f66a845172ec6f7 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:49:02 +0800 Subject: [PATCH 05/15] fix: add server-side rate limit --- .../li/cil/oc/server/DropFileManager.scala | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/server/DropFileManager.scala b/src/main/scala/li/cil/oc/server/DropFileManager.scala index 61663c6b40..568056ddeb 100644 --- a/src/main/scala/li/cil/oc/server/DropFileManager.scala +++ b/src/main/scala/li/cil/oc/server/DropFileManager.scala @@ -6,13 +6,21 @@ import net.minecraft.entity.player.EntityPlayer import java.util.UUID import java.util.concurrent.TimeUnit +import scala.collection.mutable object DropFileManager { private val sessions = CacheBuilder.newBuilder() .expireAfterAccess(8, TimeUnit.SECONDS) .build[UUID, DropFileSession]() - + private val rateLimiters = mutable.Map.empty[UUID, RateLimiter] + private def getRateLimiter(playerUUID: UUID): RateLimiter = { + rateLimiters.getOrElseUpdate(playerUUID, new RateLimiter(Settings.get.maxDropFileCount, Settings.get.maxDropFileCount)) + } def onDropFileStart(address: String, fileName: String, compressedSize: Int, player: EntityPlayer): Unit = { + if (!getRateLimiter(player.getUniqueID).tryRequest()) { + OpenComputers.log.warn(s"Player ${player.getCommandSenderName} is dropping files too fast."); + return + } if (compressedSize > Settings.get.maxDropFileSize || compressedSize < 0) { OpenComputers.log.warn(s"Rejected drop file from ${player.getCommandSenderName}: invalid compressed size $compressedSize.") return @@ -58,5 +66,24 @@ object DropFileManager { if (session != null) { sessions.invalidate(playerUUID) } + rateLimiters -= playerUUID + } + + private class RateLimiter(val maxRequests: Int, val refillPerSecond: Int) { + private var allowRequests: Double = maxRequests + private var lastRequestTime = System.currentTimeMillis() + def tryRequest(): Boolean = { + val now = System.currentTimeMillis() + val time = now - lastRequestTime + lastRequestTime = now + + allowRequests = math.min(maxRequests, allowRequests + time * refillPerSecond / 1000.0) + if (allowRequests >= 1){ + allowRequests -= 1 + true + } else { + false + } + } } } From 88c2b20429633db09379d15152212f79a993c889 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:49:15 +0800 Subject: [PATCH 06/15] fix: lower drop file chunk log level to debug to prevent log spam --- src/main/scala/li/cil/oc/server/DropFileManager.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/server/DropFileManager.scala b/src/main/scala/li/cil/oc/server/DropFileManager.scala index 568056ddeb..ddaa5036a4 100644 --- a/src/main/scala/li/cil/oc/server/DropFileManager.scala +++ b/src/main/scala/li/cil/oc/server/DropFileManager.scala @@ -43,7 +43,7 @@ object DropFileManager { sessions.invalidate(player.getUniqueID) } else { - OpenComputers.log.warn(s"Received orphan drop file chunk from ${player.getCommandSenderName}.") + OpenComputers.log.debug(s"Received orphan drop file chunk from ${player.getCommandSenderName}.") } } From f7654561644f4433951f42cba8a0653fb2829caa Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:49:29 +0800 Subject: [PATCH 07/15] fix: use readFully to ensure the whole drop file chunk is read --- src/main/scala/li/cil/oc/server/PacketHandler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/server/PacketHandler.scala b/src/main/scala/li/cil/oc/server/PacketHandler.scala index 9e30bf405b..663ac9d0d1 100644 --- a/src/main/scala/li/cil/oc/server/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/server/PacketHandler.scala @@ -218,7 +218,7 @@ object PacketHandler extends CommonPacketHandler { if ((flag & PacketFlags.DropFile.Chunk) != 0) { val size = p.readUnsignedShort() val content = new Array[Byte](size) - p.read(content) + p.readFully(content) DropFileManager.onDropFileChunk(content, p.player) } if ((flag & PacketFlags.DropFile.End) != 0) { From 611231b73e8844796287cb1d14aa6c4b4c2a6ef3 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:49:45 +0800 Subject: [PATCH 08/15] fix: add server-side file name limit --- src/main/scala/li/cil/oc/server/PacketHandler.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/server/PacketHandler.scala b/src/main/scala/li/cil/oc/server/PacketHandler.scala index 663ac9d0d1..4b8461dcce 100644 --- a/src/main/scala/li/cil/oc/server/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/server/PacketHandler.scala @@ -14,7 +14,7 @@ import li.cil.oc.common.tileentity._ import li.cil.oc.common.tileentity.traits.Computer import li.cil.oc.common.{Achievement, PacketFlags, PacketType, container, PacketHandler => CommonPacketHandler} import li.cil.oc.integration.fmp.EventHandler -import li.cil.oc.{Localization, OpenComputers, api} +import li.cil.oc.{Localization, OpenComputers, Settings, api} import net.minecraft.entity.player.{EntityPlayer, EntityPlayerMP} import net.minecraft.nbt.NBTTagCompound import net.minecraft.network.NetHandlerPlayServer @@ -212,6 +212,7 @@ object PacketHandler extends CommonPacketHandler { if ((flag & PacketFlags.DropFile.Start) != 0) { val address = p.readUTF() val fileName = p.readUTF() + if (fileName.length > Settings.get.maxDropFileNameLength) return val size = p.readInt() DropFileManager.onDropFileStart(address, fileName, size, p.player) } From fe59be0c4670cb95ef33e54dca5b86a80b640ca8 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:49:55 +0800 Subject: [PATCH 09/15] fix: rename tooMuchFiles to tooManyFiles --- src/main/resources/assets/opencomputers/lang/en_US.lang | 2 +- src/main/scala/li/cil/oc/Localization.scala | 5 ++--- src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/resources/assets/opencomputers/lang/en_US.lang b/src/main/resources/assets/opencomputers/lang/en_US.lang index d9087bf0ce..9b0759b8d0 100644 --- a/src/main/resources/assets/opencomputers/lang/en_US.lang +++ b/src/main/resources/assets/opencomputers/lang/en_US.lang @@ -257,7 +257,7 @@ oc:gui.Switch.QueueSize=Queue size oc:gui.Switch.TransferRate=Cycle rate oc:gui.Terminal.InvalidKey=Invalid key, most likely another terminal has been bound to the server. oc:gui.Terminal.OutOfRange=No signal. -oc:gui.InputBuffer.TooMuchFiles=File count exceeds the maximum of %s. +oc:gui.InputBuffer.TooManyFiles=File count exceeds the maximum of %s. oc:gui.InputBuffer.FileTooLarge=File size exceeds the maximum of %s bytes. oc:gui.InputBuffer.FileNameTooLong=File name length exceeds the maximum of %s characters. diff --git a/src/main/scala/li/cil/oc/Localization.scala b/src/main/scala/li/cil/oc/Localization.scala index be66e13cba..e1577a9afb 100644 --- a/src/main/scala/li/cil/oc/Localization.scala +++ b/src/main/scala/li/cil/oc/Localization.scala @@ -165,9 +165,8 @@ object Localization { def OutOfRange = localizeLater("gui.Terminal.OutOfRange") } - object InputBuffer - { - def TooMuchFiles = localizeLater("gui.InputBuffer.TooMuchFiles", Settings.get.maxDropFileCount.toString) + object InputBuffer { + def TooManyFiles = localizeLater("gui.InputBuffer.TooMuchFiles", Settings.get.maxDropFileCount.toString) def FileTooLarge = localizeLater("gui.InputBuffer.FileTooLarge", Settings.get.maxDropFileSize.toString) def FileNameTooLong = localizeLater("gui.InputBuffer.FileNameTooLong", Settings.get.maxDropFileNameLength.toString) } diff --git a/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala b/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala index e49b996549..53f11264f1 100644 --- a/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala +++ b/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala @@ -159,7 +159,7 @@ trait InputBuffer extends DisplayBuffer { val allFiles = getFiles(filePath) if (allFiles.size > Settings.get.maxDropFileCount) { EventHandler.scheduleClient(() => { - this.mc.thePlayer.addChatMessage(Localization.InputBuffer.TooMuchFiles) + this.mc.thePlayer.addChatMessage(Localization.InputBuffer.TooManyFiles) playErrorSound() }) } From 25c775e027250266547eb11102687aa2a10f86b6 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:51:11 +0800 Subject: [PATCH 10/15] fix: add player feedback and config clamping for drop file size --- src/main/resources/assets/opencomputers/lang/en_US.lang | 1 + src/main/scala/li/cil/oc/Localization.scala | 1 + src/main/scala/li/cil/oc/Settings.scala | 2 +- src/main/scala/li/cil/oc/server/DropFileManager.scala | 6 +++++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/resources/assets/opencomputers/lang/en_US.lang b/src/main/resources/assets/opencomputers/lang/en_US.lang index 9b0759b8d0..2d10ea5f0c 100644 --- a/src/main/resources/assets/opencomputers/lang/en_US.lang +++ b/src/main/resources/assets/opencomputers/lang/en_US.lang @@ -258,6 +258,7 @@ oc:gui.Switch.TransferRate=Cycle rate oc:gui.Terminal.InvalidKey=Invalid key, most likely another terminal has been bound to the server. oc:gui.Terminal.OutOfRange=No signal. oc:gui.InputBuffer.TooManyFiles=File count exceeds the maximum of %s. +oc:gui.InputBuffer.TooFrequentFiles=File upload frequency exceeds the maximum of %s files per second. oc:gui.InputBuffer.FileTooLarge=File size exceeds the maximum of %s bytes. oc:gui.InputBuffer.FileNameTooLong=File name length exceeds the maximum of %s characters. diff --git a/src/main/scala/li/cil/oc/Localization.scala b/src/main/scala/li/cil/oc/Localization.scala index e1577a9afb..19c77a7e68 100644 --- a/src/main/scala/li/cil/oc/Localization.scala +++ b/src/main/scala/li/cil/oc/Localization.scala @@ -167,6 +167,7 @@ object Localization { object InputBuffer { def TooManyFiles = localizeLater("gui.InputBuffer.TooMuchFiles", Settings.get.maxDropFileCount.toString) + def TooFrequentFiles = localizeLater("gui.InputBuffer.TooFrequentFiles", Settings.get.maxDropFileCount.toString) def FileTooLarge = localizeLater("gui.InputBuffer.FileTooLarge", Settings.get.maxDropFileSize.toString) def FileNameTooLong = localizeLater("gui.InputBuffer.FileNameTooLong", Settings.get.maxDropFileNameLength.toString) } diff --git a/src/main/scala/li/cil/oc/Settings.scala b/src/main/scala/li/cil/oc/Settings.scala index 63e7586739..f4feeda6a0 100644 --- a/src/main/scala/li/cil/oc/Settings.scala +++ b/src/main/scala/li/cil/oc/Settings.scala @@ -345,7 +345,7 @@ class Settings(val config: Config) { val inputUsername = config.getBoolean("misc.inputUsername") val maxDropFileCount = config.getInt("misc.maxDropFileCount") max 0 val maxDropFileNameLength = 128 - val maxDropFileSize = config.getInt("misc.maxDropFileSize") min 4 * 1024 * 1024 + val maxDropFileSize = config.getInt("misc.maxDropFileSize") max 0 min 4 * 1024 * 1024 val enableClipboardBatching = config.getBoolean("misc.enableClipboardBatching") val maxClipboardSize = config.getInt("misc.maxClipboardSize") val clipboardBatchSize = config.getInt("misc.clipboardBatchSize") max 1 diff --git a/src/main/scala/li/cil/oc/server/DropFileManager.scala b/src/main/scala/li/cil/oc/server/DropFileManager.scala index ddaa5036a4..fa4d8def70 100644 --- a/src/main/scala/li/cil/oc/server/DropFileManager.scala +++ b/src/main/scala/li/cil/oc/server/DropFileManager.scala @@ -1,7 +1,7 @@ package li.cil.oc.server import com.google.common.cache.CacheBuilder -import li.cil.oc.{OpenComputers, Settings, api} +import li.cil.oc.{Localization, OpenComputers, Settings, api} import net.minecraft.entity.player.EntityPlayer import java.util.UUID @@ -16,13 +16,16 @@ object DropFileManager { private def getRateLimiter(playerUUID: UUID): RateLimiter = { rateLimiters.getOrElseUpdate(playerUUID, new RateLimiter(Settings.get.maxDropFileCount, Settings.get.maxDropFileCount)) } + def onDropFileStart(address: String, fileName: String, compressedSize: Int, player: EntityPlayer): Unit = { if (!getRateLimiter(player.getUniqueID).tryRequest()) { OpenComputers.log.warn(s"Player ${player.getCommandSenderName} is dropping files too fast."); + player.addChatMessage(Localization.InputBuffer.TooFrequentFiles) return } if (compressedSize > Settings.get.maxDropFileSize || compressedSize < 0) { OpenComputers.log.warn(s"Rejected drop file from ${player.getCommandSenderName}: invalid compressed size $compressedSize.") + player.addChatMessage(Localization.InputBuffer.FileTooLarge) return } ComponentTracker.get(player.worldObj, address) match { @@ -50,6 +53,7 @@ object DropFileManager { def onDropFileEnd(unCompressedSize: Int, player: EntityPlayer): Unit = { if (unCompressedSize > Settings.get.maxDropFileSize || unCompressedSize < 0) { OpenComputers.log.warn(s"Rejected drop file from ${player.getCommandSenderName}: invalid uncompressed size $unCompressedSize.") + player.addChatMessage(Localization.InputBuffer.FileTooLarge) return } val session = sessions.getIfPresent(player.getUniqueID) From 465f0df652047ace5cbdc1d255c44311b69fb9d5 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:52:43 +0800 Subject: [PATCH 11/15] fix: correct size arithmetic --- src/main/scala/li/cil/oc/client/PacketSender.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/client/PacketSender.scala b/src/main/scala/li/cil/oc/client/PacketSender.scala index 133c42192d..4175dd8285 100644 --- a/src/main/scala/li/cil/oc/client/PacketSender.scala +++ b/src/main/scala/li/cil/oc/client/PacketSender.scala @@ -107,7 +107,7 @@ object PacketSender { val stream = new DeflaterOutputStream(data, new Deflater(Deflater.BEST_SPEED)) stream.write(content) stream.close() - // 1(compress) + 38(address) + 514(name) + 8(size) + 1(flag) + 8(size) = 570 + // 1(compress) + 38(address) + 386(name) + 4(size) + 1(flag) + 4(size) = 434 val chunks = data.toByteArray.grouped(31 * 1024).toArray val size = chunks.map(_.length).sum for (i <- chunks.indices) { From 87574c893abb01f1197e49182901c7ae2d036168 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:54:42 +0800 Subject: [PATCH 12/15] style: clean up duplicate imports --- src/main/scala/li/cil/oc/client/PacketSender.scala | 2 -- src/main/scala/li/cil/oc/server/PacketHandler.scala | 1 - 2 files changed, 3 deletions(-) diff --git a/src/main/scala/li/cil/oc/client/PacketSender.scala b/src/main/scala/li/cil/oc/client/PacketSender.scala index 4175dd8285..278a303d50 100644 --- a/src/main/scala/li/cil/oc/client/PacketSender.scala +++ b/src/main/scala/li/cil/oc/client/PacketSender.scala @@ -1,6 +1,5 @@ package li.cil.oc.client -import cpw.mods.fml.common.network.internal.FMLProxyPacket import li.cil.oc.{Localization, Settings} import li.cil.oc.common.{CompressedPacketBuilder, PacketFlags, PacketType, SimplePacketBuilder} import li.cil.oc.common.entity.Drone @@ -13,7 +12,6 @@ import net.minecraft.util.ResourceLocation import net.minecraftforge.common.util.ForgeDirection import java.io.ByteArrayOutputStream -import java.nio.charset.StandardCharsets import java.util.zip.{Deflater, DeflaterOutputStream} object PacketSender { diff --git a/src/main/scala/li/cil/oc/server/PacketHandler.scala b/src/main/scala/li/cil/oc/server/PacketHandler.scala index 4b8461dcce..99b17a9865 100644 --- a/src/main/scala/li/cil/oc/server/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/server/PacketHandler.scala @@ -14,7 +14,6 @@ import li.cil.oc.common.tileentity._ import li.cil.oc.common.tileentity.traits.Computer import li.cil.oc.common.{Achievement, PacketFlags, PacketType, container, PacketHandler => CommonPacketHandler} import li.cil.oc.integration.fmp.EventHandler -import li.cil.oc.{Localization, OpenComputers, Settings, api} import net.minecraft.entity.player.{EntityPlayer, EntityPlayerMP} import net.minecraft.nbt.NBTTagCompound import net.minecraft.network.NetHandlerPlayServer From b4cfa0fb5de0750a4b214536d7522b91e9e0f9b4 Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:55:41 +0800 Subject: [PATCH 13/15] fix: complete rename of TooMuchFiles to TooManyFiles --- src/main/scala/li/cil/oc/Localization.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/Localization.scala b/src/main/scala/li/cil/oc/Localization.scala index 19c77a7e68..134b779540 100644 --- a/src/main/scala/li/cil/oc/Localization.scala +++ b/src/main/scala/li/cil/oc/Localization.scala @@ -166,7 +166,7 @@ object Localization { } object InputBuffer { - def TooManyFiles = localizeLater("gui.InputBuffer.TooMuchFiles", Settings.get.maxDropFileCount.toString) + def TooManyFiles = localizeLater("gui.InputBuffer.TooManyFiles", Settings.get.maxDropFileCount.toString) def TooFrequentFiles = localizeLater("gui.InputBuffer.TooFrequentFiles", Settings.get.maxDropFileCount.toString) def FileTooLarge = localizeLater("gui.InputBuffer.FileTooLarge", Settings.get.maxDropFileSize.toString) def FileNameTooLong = localizeLater("gui.InputBuffer.FileNameTooLong", Settings.get.maxDropFileNameLength.toString) From 98612cd236c30491d0341c9e69dc74652f3477fe Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:01:49 +0800 Subject: [PATCH 14/15] refactor: remove the useless EntityPlayer lookup --- .../li/cil/oc/server/DropFileManager.scala | 10 ++-- .../li/cil/oc/server/DropFileSession.scala | 47 +++++++------------ 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/main/scala/li/cil/oc/server/DropFileManager.scala b/src/main/scala/li/cil/oc/server/DropFileManager.scala index fa4d8def70..eada08fd84 100644 --- a/src/main/scala/li/cil/oc/server/DropFileManager.scala +++ b/src/main/scala/li/cil/oc/server/DropFileManager.scala @@ -19,7 +19,7 @@ object DropFileManager { def onDropFileStart(address: String, fileName: String, compressedSize: Int, player: EntityPlayer): Unit = { if (!getRateLimiter(player.getUniqueID).tryRequest()) { - OpenComputers.log.warn(s"Player ${player.getCommandSenderName} is dropping files too fast."); + OpenComputers.log.warn(s"Player ${player.getCommandSenderName} is dropping files too fast.") player.addChatMessage(Localization.InputBuffer.TooFrequentFiles) return } @@ -32,7 +32,7 @@ object DropFileManager { case Some(buffer: api.internal.TextBuffer) => if (sessions.getIfPresent(player.getUniqueID) != null) OpenComputers.log.warn(s"Player ${player.getCommandSenderName} started a new drop file before finishing the previous one. Overwriting.") - val session = new DropFileSession(fileName, compressedSize, player.getUniqueID, player.getCommandSenderName, buffer) + val session = new DropFileSession(fileName, compressedSize, buffer) sessions.put(player.getUniqueID, session) case _ => OpenComputers.log.warn(s"Drop file target not found for address $address") @@ -42,7 +42,7 @@ object DropFileManager { def onDropFileChunk(data: Array[Byte], player: EntityPlayer): Unit = { val session = sessions.getIfPresent(player.getUniqueID) if (session != null) { - if (!session.onDropFileChunk(data)) + if (!session.onDropFileChunk(data, player)) sessions.invalidate(player.getUniqueID) } else { @@ -58,7 +58,7 @@ object DropFileManager { } val session = sessions.getIfPresent(player.getUniqueID) if (session != null) { - session.onDropFileEnd(unCompressedSize) + session.onDropFileEnd(unCompressedSize, player) sessions.invalidate(player.getUniqueID) } else { OpenComputers.log.warn(s"Received orphan drop file end from ${player.getCommandSenderName}.") @@ -82,7 +82,7 @@ object DropFileManager { lastRequestTime = now allowRequests = math.min(maxRequests, allowRequests + time * refillPerSecond / 1000.0) - if (allowRequests >= 1){ + if (allowRequests >= 1) { allowRequests -= 1 true } else { diff --git a/src/main/scala/li/cil/oc/server/DropFileSession.scala b/src/main/scala/li/cil/oc/server/DropFileSession.scala index 97096b8841..d89fbd4844 100644 --- a/src/main/scala/li/cil/oc/server/DropFileSession.scala +++ b/src/main/scala/li/cil/oc/server/DropFileSession.scala @@ -1,57 +1,42 @@ package li.cil.oc.server import li.cil.oc.{OpenComputers, api} -import net.minecraft.entity.player.EntityPlayerMP -import net.minecraft.server.MinecraftServer +import net.minecraft.entity.player.EntityPlayer import org.apache.commons.io.IOUtils import org.apache.commons.io.input.BoundedInputStream -import scala.collection.JavaConverters._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream} -import java.util.UUID import java.util.zip.InflaterInputStream -class DropFileSession(fileName: String, compressedSize: Int, playerUUID: UUID, playerName: String, target: api.internal.TextBuffer) { +class DropFileSession(fileName: String, compressedSize: Int, target: api.internal.TextBuffer) { private val compressed = new ByteArrayOutputStream(math.min(compressedSize, 32 * 1024)) - def onDropFileChunk(data: Array[Byte]): Boolean = { + def onDropFileChunk(data: Array[Byte], player: EntityPlayer): Boolean = { if (compressed.size() + data.length <= compressedSize) { compressed.write(data) true } else { - OpenComputers.log.warn(s"Receive a corrupt drop file packet from $playerName:$playerUUID : buffer overflow.") + OpenComputers.log.warn(s"Receive a corrupt drop file packet from ${player.getCommandSenderName} : buffer overflow.") false } } - def onDropFileEnd(unCompressedSize: Int): Unit = { + def onDropFileEnd(unCompressedSize: Int, player: EntityPlayer): Unit = { if (compressed.size() != compressedSize) { - OpenComputers.log.warn(s"Incomplete drop file packet from $playerName:$playerUUID.") + OpenComputers.log.warn(s"Incomplete drop file packet from ${player.getCommandSenderName}.") return } - val player = for { - server <- Option(MinecraftServer.getServer) - p <- server.getConfigurationManager.playerEntityList.asScala.collectFirst { - case p: EntityPlayerMP if p.getUniqueID == playerUUID => p - } - } yield p - - player match { - case Some (player) => - val uncompressed = new InflaterInputStream(new ByteArrayInputStream(compressed.toByteArray)) - try { - val content = IOUtils.toByteArray(new BoundedInputStream(uncompressed, unCompressedSize.toLong + 1)) - if (content.length == unCompressedSize) - target.dropFile(fileName, content, player) - else - OpenComputers.log.warn(s"Receive a corrupt drop file packet from $playerName:$playerUUID. Decompressed size mismatch! Expected: $unCompressedSize, actually: ${content.length}.") - } finally { - uncompressed.close() - compressed.reset() - } - case None => - OpenComputers.log.debug(s"Player $playerName:$playerUUID disconnected before drop file finished.") + val uncompressed = new InflaterInputStream(new ByteArrayInputStream(compressed.toByteArray)) + try { + val content = IOUtils.toByteArray(new BoundedInputStream(uncompressed, unCompressedSize.toLong + 1)) + if (content.length == unCompressedSize) + target.dropFile(fileName, content, player) + else + OpenComputers.log.warn(s"Receive a corrupt drop file packet from ${player.getCommandSenderName}. Decompressed size mismatch! Expected: $unCompressedSize, actually: ${content.length}.") + } finally { + uncompressed.close() + compressed.reset() } } } From fd96ccad551ce77d95c65db39426c321f56894ba Mon Sep 17 00:00:00 2001 From: hinyb <40139991+hinyb@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:12:07 +0800 Subject: [PATCH 15/15] fix: add log for oversized file name --- src/main/scala/li/cil/oc/server/DropFileManager.scala | 4 ++++ src/main/scala/li/cil/oc/server/PacketHandler.scala | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/server/DropFileManager.scala b/src/main/scala/li/cil/oc/server/DropFileManager.scala index eada08fd84..5b69df37ee 100644 --- a/src/main/scala/li/cil/oc/server/DropFileManager.scala +++ b/src/main/scala/li/cil/oc/server/DropFileManager.scala @@ -18,6 +18,10 @@ object DropFileManager { } def onDropFileStart(address: String, fileName: String, compressedSize: Int, player: EntityPlayer): Unit = { + if (fileName.length > Settings.get.maxDropFileNameLength) { + OpenComputers.log.warn(s"Rejected drop file from ${player.getCommandSenderName}: File name is too long!") + return + } if (!getRateLimiter(player.getUniqueID).tryRequest()) { OpenComputers.log.warn(s"Player ${player.getCommandSenderName} is dropping files too fast.") player.addChatMessage(Localization.InputBuffer.TooFrequentFiles) diff --git a/src/main/scala/li/cil/oc/server/PacketHandler.scala b/src/main/scala/li/cil/oc/server/PacketHandler.scala index 99b17a9865..fefeb9a8d4 100644 --- a/src/main/scala/li/cil/oc/server/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/server/PacketHandler.scala @@ -211,7 +211,6 @@ object PacketHandler extends CommonPacketHandler { if ((flag & PacketFlags.DropFile.Start) != 0) { val address = p.readUTF() val fileName = p.readUTF() - if (fileName.length > Settings.get.maxDropFileNameLength) return val size = p.readInt() DropFileManager.onDropFileStart(address, fileName, size, p.player) }