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 73d919519a..d05698f0e1 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1274,6 +1274,13 @@ opencomputers { # Note: Only applied if enableClipboardBatching = true. clipboardBatchSize: 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..2d10ea5f0c 100644 --- a/src/main/resources/assets/opencomputers/lang/en_US.lang +++ b/src/main/resources/assets/opencomputers/lang/en_US.lang @@ -257,6 +257,10 @@ 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.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. # 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..134b779540 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 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) + } + 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 9cd46b1216..f4feeda6a0 100644 --- a/src/main/scala/li/cil/oc/Settings.scala +++ b/src/main/scala/li/cil/oc/Settings.scala @@ -343,6 +343,9 @@ class Settings(val config: Config) { val maxScreenWidth = config.getInt("misc.maxScreenWidth") max 1 val maxScreenHeight = config.getInt("misc.maxScreenHeight") max 1 val inputUsername = config.getBoolean("misc.inputUsername") + val maxDropFileCount = config.getInt("misc.maxDropFileCount") max 0 + val maxDropFileNameLength = 128 + 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/client/PacketSender.scala b/src/main/scala/li/cil/oc/client/PacketSender.scala index 552b377245..278a303d50 100644 --- a/src/main/scala/li/cil/oc/client/PacketSender.scala +++ b/src/main/scala/li/cil/oc/client/PacketSender.scala @@ -1,9 +1,7 @@ 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 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 +11,9 @@ import net.minecraft.item.ItemStack import net.minecraft.util.ResourceLocation import net.minecraftforge.common.util.ForgeDirection +import java.io.ByteArrayOutputStream +import java.util.zip.{Deflater, DeflaterOutputStream} + object PacketSender { // Timestamp after which the next clipboard message may be sent. Used to // avoid spamming large packets on key repeat. @@ -70,13 +71,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 > Settings.get.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)) - } + if (value.length > Settings.get.maxClipboardLength || System.currentTimeMillis() < clipboardCooldown) + playErrorSound() else { clipboardCooldown = System.currentTimeMillis() + value.length / 10 for (part <- value.grouped(16 * 1024)) { @@ -91,21 +95,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) + 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) { + 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..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 @@ -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.TooManyFiles) + 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/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/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..5b69df37ee --- /dev/null +++ b/src/main/scala/li/cil/oc/server/DropFileManager.scala @@ -0,0 +1,97 @@ +package li.cil.oc.server + +import com.google.common.cache.CacheBuilder +import li.cil.oc.{Localization, OpenComputers, Settings, api} +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 (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) + 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 { + 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, 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, player)) + sessions.invalidate(player.getUniqueID) + } + else { + OpenComputers.log.debug(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.") + player.addChatMessage(Localization.InputBuffer.FileTooLarge) + return + } + val session = sessions.getIfPresent(player.getUniqueID) + if (session != null) { + session.onDropFileEnd(unCompressedSize, player) + sessions.invalidate(player.getUniqueID) + } else { + 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) + } + 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 + } + } + } +} 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..d89fbd4844 --- /dev/null +++ b/src/main/scala/li/cil/oc/server/DropFileSession.scala @@ -0,0 +1,42 @@ +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 + +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], 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 ${player.getCommandSenderName} : buffer overflow.") + false + } + } + + def onDropFileEnd(unCompressedSize: Int, player: EntityPlayer): 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(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() + } + } +} diff --git a/src/main/scala/li/cil/oc/server/PacketHandler.scala b/src/main/scala/li/cil/oc/server/PacketHandler.scala index a730814ab9..fefeb9a8d4 100644 --- a/src/main/scala/li/cil/oc/server/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/server/PacketHandler.scala @@ -5,20 +5,16 @@ import cpw.mods.fml.common.network.FMLNetworkEvent.ServerCustomPacketEvent import li.cil.oc.{Localization, OpenComputers, Settings, 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 net.minecraft.entity.player.{EntityPlayer, EntityPlayerMP} import net.minecraft.nbt.NBTTagCompound import net.minecraft.network.NetHandlerPlayServer import net.minecraft.world.WorldServer @@ -27,12 +23,6 @@ import org.apache.logging.log4j.MarkerManager object PacketHandler extends CommonPacketHandler { private val securityMarker = MarkerManager.getMarker("SuspiciousPackets") - - // Server-side cap on client-supplied text (clipboard paste, dropped files). - // The client enforces the same 64KB limit, but a modified client can omit it, - // so we must re-check here rather than trust the sender. - private val maxClientTextLength = 64 * 1024 - private def isFinite(f: Float): Boolean = !f.isNaN && !f.isInfinity private def isPlayerWatchingHost(player: EntityPlayerMP, host: api.network.EnvironmentHost): Boolean = host.world match { @@ -104,7 +94,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 } @@ -217,13 +207,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.readFully(content) + DropFileManager.onDropFileChunk(content, p.player) + } + if ((flag & PacketFlags.DropFile.End) != 0) { + val size = p.readInt() + DropFileManager.onDropFileEnd(size, p.player) } } @@ -275,7 +274,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. } } @@ -311,7 +310,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)