/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* * Copyright (c) 2011-2014 Glowstone - Tad Hardesty * Copyright (c) 2010-2011 Lightstone - Graham Edgecombe * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.network.pipeline; import static org.lanternpowered.server.network.buffer.LanternByteBuffer.readVarInt; import static org.lanternpowered.server.network.buffer.LanternByteBuffer.writeVarInt; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.EncoderException; import io.netty.handler.codec.MessageToMessageCodec; import java.util.List; import java.util.zip.Deflater; import java.util.zip.Inflater; public final class MessageCompressionHandler extends MessageToMessageCodec<ByteBuf, ByteBuf> { private final Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION); private final Inflater inflater = new Inflater(); private final int compressionThreshold; public MessageCompressionHandler(int compressionThreshold) { this.compressionThreshold = compressionThreshold; } @Override protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { ByteBuf prefixBuf = ctx.alloc().buffer(5); ByteBuf contentsBuf; if (msg.readableBytes() >= this.compressionThreshold) { // Message should be compressed int index = msg.readerIndex(); int length = msg.readableBytes(); byte[] sourceData = new byte[length]; msg.readBytes(sourceData); this.deflater.setInput(sourceData); this.deflater.finish(); byte[] compressedData = new byte[length]; int compressedLength = this.deflater.deflate(compressedData); this.deflater.reset(); if (compressedLength == 0) { // Compression failed in some weird way throw new EncoderException("Failed to compress message of size " + length); } else if (compressedLength >= length) { // Compression increased the size. threshold is probably too low // Send as an uncompressed packet writeVarInt(prefixBuf, 0); msg.readerIndex(index); msg.retain(); contentsBuf = msg; } else { // All is well writeVarInt(prefixBuf, length); contentsBuf = Unpooled.wrappedBuffer(compressedData, 0, compressedLength); } } else { // Message should be sent through writeVarInt(prefixBuf, 0); msg.retain(); contentsBuf = msg; } out.add(Unpooled.wrappedBuffer(prefixBuf, contentsBuf)); } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { int index = msg.readerIndex(); int uncompressedSize = readVarInt(msg); if (uncompressedSize == 0) { // Message is uncompressed int length = msg.readableBytes(); if (length >= this.compressionThreshold) { // Invalid throw new DecoderException("Received uncompressed message of size " + length + " greater than threshold " + this.compressionThreshold); } ByteBuf buf = ctx.alloc().buffer(length); msg.readBytes(buf, length); out.add(buf); } else { // Message is compressed byte[] sourceData = new byte[msg.readableBytes()]; msg.readBytes(sourceData); this.inflater.setInput(sourceData); byte[] destData = new byte[uncompressedSize]; int resultLength = this.inflater.inflate(destData); this.inflater.reset(); if (resultLength == 0) { // Might be a leftover from before compression was enabled (no compression header) // UncompressedSize is likely to be < threshold msg.readerIndex(index); msg.retain(); out.add(msg); } else if (resultLength != uncompressedSize) { throw new DecoderException("Received compressed message claiming to be of size " + uncompressedSize + " but actually " + resultLength); } else { out.add(Unpooled.wrappedBuffer(destData)); } } } }