Modularise! Also add some unit tests.

This commit is contained in:
Major-
2015-05-26 13:49:27 +01:00
parent 902a203861
commit e4778105f5
658 changed files with 1532 additions and 1004 deletions
@@ -0,0 +1,51 @@
package org.apollo.net;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.timeout.IdleStateHandler;
/**
* A {@link ChannelInitializer} for the HTTP protocol.
*
* @author Graham
*/
public final class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
/**
* The maximum length of a request, in bytes.
*/
private static final int MAX_REQUEST_LENGTH = 8192;
/**
* The server event handler.
*/
private final ChannelInboundHandlerAdapter handler;
/**
* Creates the HTTP pipeline factory.
*
* @param handler The file server event handler.
*/
public HttpChannelInitializer(ChannelInboundHandlerAdapter handler) {
this.handler = handler;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("chunker", new HttpObjectAggregator(MAX_REQUEST_LENGTH));
pipeline.addLast("encoder", new HttpResponseEncoder());
pipeline.addLast("timeout", new IdleStateHandler(NetworkConstants.IDLE_TIME, 0, 0));
pipeline.addLast("handler", handler);
}
}
@@ -0,0 +1,77 @@
package org.apollo.net;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import java.nio.charset.Charset;
import org.apollo.net.codec.jaggrab.JagGrabRequestDecoder;
import org.apollo.net.codec.jaggrab.JagGrabResponseEncoder;
import com.google.common.base.Charsets;
/**
* A {@link ChannelInitializer} for the JAGGRAB protocol.
*
* @author Graham
*/
public final class JagGrabChannelInitializer extends ChannelInitializer<SocketChannel> {
/**
* A buffer with two line feed (LF) characters in it.
*/
private static final ByteBuf DOUBLE_LINE_FEED_DELIMITER = Unpooled.buffer(2);
/**
* The character set used in the request.
*/
private static final Charset JAGGRAB_CHARSET = Charsets.US_ASCII;
/**
* The maximum length of a request, in bytes.
*/
private static final int MAX_REQUEST_LENGTH = 8192;
/**
* Populates the double line feed buffer.
*/
static {
DOUBLE_LINE_FEED_DELIMITER.writeByte(10).writeByte(10);
}
/**
* The file server event handler.
*/
private final ChannelInboundHandlerAdapter handler;
/**
* Creates a {@code JAGGRAB} pipeline factory.
*
* @param handler The file server event handler.
*/
public JagGrabChannelInitializer(ChannelInboundHandlerAdapter handler) {
this.handler = handler;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(MAX_REQUEST_LENGTH, DOUBLE_LINE_FEED_DELIMITER));
pipeline.addLast("string-decoder", new StringDecoder(JAGGRAB_CHARSET));
pipeline.addLast("jaggrab-decoder", new JagGrabRequestDecoder());
pipeline.addLast("jaggrab-encoder", new JagGrabResponseEncoder());
pipeline.addLast("timeout", new IdleStateHandler(NetworkConstants.IDLE_TIME, 0, 0));
pipeline.addLast("handler", handler);
}
}
@@ -0,0 +1,88 @@
package org.apollo.net;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import org.apollo.util.xml.XmlNode;
import org.apollo.util.xml.XmlParser;
import com.google.common.base.Preconditions;
/**
* Holds various network-related constants such as port numbers.
*
* @author Graham
* @author Major
*/
public final class NetworkConstants {
/**
* The HTTP port.
*/
public static final int HTTP_PORT;
/**
* The number of seconds before a connection becomes idle.
*/
public static final int IDLE_TIME = 15;
/**
* The JAGGRAB port.
*/
public static final int JAGGRAB_PORT;
/**
* The exponent used when decrypting the RSA block.
*/
public static final BigInteger RSA_EXPONENT;
/**
* The modulus used when decrypting the RSA block.
*/
public static final BigInteger RSA_MODULUS;
/**
* The service port.
*/
public static final int SERVICE_PORT;
static {
try (InputStream is = new FileInputStream("data/net.xml")) {
XmlNode net = new XmlParser().parse(is);
if (!net.getName().equals("net")) {
throw new IOException("Root node name is not 'net'.");
}
XmlNode rsa = net.getChild("rsa");
Preconditions.checkState(rsa != null, "Root node must have a child named 'rsa'.");
XmlNode modulus = rsa.getChild("modulus"), exponent = rsa.getChild("private-exponent");
Preconditions.checkState(modulus != null && exponent != null, "Rsa node must have two children: 'modulus' and 'private-exponent'.");
RSA_MODULUS = new BigInteger(modulus.getValue());
RSA_EXPONENT = new BigInteger(exponent.getValue());
XmlNode ports = net.getChild("ports");
Preconditions.checkState(ports != null, "Root node must have a child named 'ports'.");
XmlNode http = ports.getChild("http"), service = ports.getChild("service"), jaggrab = ports.getChild("jaggrab");
Preconditions.checkState(http != null && service != null && jaggrab != null, "Ports node must have three children: 'http', 'service', and 'jaggrab'.");
HTTP_PORT = Integer.parseInt(http.getValue());
SERVICE_PORT = Integer.parseInt(service.getValue());
JAGGRAB_PORT = Integer.parseInt(jaggrab.getValue());
} catch (Exception exception) {
throw new ExceptionInInitializerError(new IOException("Error parsing net.xml.", exception));
}
}
/**
* Sole private constructor to prevent instantiation.
*/
private NetworkConstants() {
}
}
@@ -0,0 +1,40 @@
package org.apollo.net;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import org.apollo.net.codec.handshake.HandshakeDecoder;
/**
* A {@link ChannelInitializer} for the service pipeline.
*
* @author Graham
*/
public final class ServiceChannelInitializer extends ChannelInitializer<SocketChannel> {
/**
* The network event handler.
*/
private final ChannelInboundHandlerAdapter handler;
/**
* Creates the service pipeline factory.
*
* @param handler The networking event handler.
*/
public ServiceChannelInitializer(ChannelInboundHandlerAdapter handler) {
this.handler = handler;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("handshakeDecoder", new HandshakeDecoder());
pipeline.addLast("timeout", new IdleStateHandler(NetworkConstants.IDLE_TIME, 0, 0));
pipeline.addLast("handler", handler);
}
}
@@ -0,0 +1,20 @@
package org.apollo.net.codec.game;
/**
* An enumeration which holds the mode a {@link GamePacketBuilder} or {@link GamePacketReader} can be in.
*
* @author Graham
*/
public enum AccessMode {
/**
* When in bit access mode, bits can be written and packed into bytes.
*/
BIT_ACCESS,
/**
* When in byte access modes, bytes are written directly to the buffer.
*/
BYTE_ACCESS;
}
@@ -0,0 +1,31 @@
package org.apollo.net.codec.game;
/**
* A class holding data-related constants.
*
* @author Graham
*/
public final class DataConstants {
/**
* An array of bit masks. The element {@code n} is equal to {@code 2<sup>n</sup> - 1}.
*/
public static final int[] BIT_MASK = new int[32];
/**
* Initializes the {@link #BIT_MASK} array.
*/
static {
for (int i = 0; i < BIT_MASK.length; i++) {
BIT_MASK[i] = (1 << i) - 1;
}
}
/**
* Default private constructor to prevent instantiation.
*/
private DataConstants() {
}
}
@@ -0,0 +1,30 @@
package org.apollo.net.codec.game;
/**
* Represents the order of bytes in a {@link DataType} when {@link DataType#getBytes()} {@code > 1}.
*
* @author Graham
*/
public enum DataOrder {
/**
* Most significant byte to least significant byte.
*/
BIG,
/**
* Also known as the V2 order.
*/
INVERSED_MIDDLE,
/**
* Least significant byte to most significant byte.
*/
LITTLE,
/**
* Also known as the V1 order.
*/
MIDDLE;
}
@@ -0,0 +1,30 @@
package org.apollo.net.codec.game;
/**
* Represents the different ways data values can be transformed.
*
* @author Graham
*/
public enum DataTransformation {
/**
* Adds 128 to the value when it is written, takes 128 from the value when it is read (also known as type-A).
*/
ADD,
/**
* Negates the value (also known as type-C).
*/
NEGATE,
/**
* No transformation is done.
*/
NONE,
/**
* Subtracts the value from 128 (also known as type-S).
*/
SUBTRACT;
}
@@ -0,0 +1,58 @@
package org.apollo.net.codec.game;
/**
* Represents the different simple data types.
*
* @author Graham
*/
public enum DataType {
/**
* A byte.
*/
BYTE(1),
/**
* A short.
*/
SHORT(2),
/**
* A 'tri byte' - a group of three bytes.
*/
TRI_BYTE(3),
/**
* An integer.
*/
INT(4),
/**
* A long.
*/
LONG(8);
/**
* The number of bytes this type occupies.
*/
private final int bytes;
/**
* Creates a data type.
*
* @param bytes The number of bytes it occupies.
*/
private DataType(int bytes) {
this.bytes = bytes;
}
/**
* Gets the number of bytes the data type occupies.
*
* @return The number of bytes.
*/
public int getBytes() {
return bytes;
}
}
@@ -0,0 +1,28 @@
package org.apollo.net.codec.game;
/**
* An enumeration with the different states the {@link GamePacketDecoder} can be in.
*
* @author Graham
*/
public enum GameDecoderState {
/**
* The game length state waits for the packet length. Once it has been received, it sets the state to the payload
* state.
*/
GAME_LENGTH,
/**
* The game opcode state waits for an encrypted opcode. It decrypts it, and will either set the next state to the
* length (if the packet is variably- sized) or the payload (if it is not variably-sized) state.
*/
GAME_OPCODE,
/**
* The payload state will wait for the whole packet to be received. Then, it will pass a {@link GamePacket} object
* to Netty and reset the state back to the game opcode state, ready for the next packet.
*/
GAME_PAYLOAD;
}
@@ -0,0 +1,43 @@
package org.apollo.net.codec.game;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
import org.apollo.net.message.Message;
import org.apollo.net.release.MessageDecoder;
import org.apollo.net.release.Release;
/**
* A {@link MessageToMessageDecoder} that decodes {@link GamePacket}s into {@link Message}s.
*
* @author Graham
*/
public final class GameMessageDecoder extends MessageToMessageDecoder<GamePacket> {
/**
* The current release.
*/
private final Release release;
/**
* Creates the game message decoder with the specified release.
*
* @param release The release.
*/
public GameMessageDecoder(Release release) {
this.release = release;
}
@Override
protected void decode(ChannelHandlerContext ctx, GamePacket packet, List<Object> out) {
MessageDecoder<?> decoder = release.getMessageDecoder(packet.getOpcode());
if (decoder != null) {
out.add(decoder.decode(packet));
} else {
System.out.println("Unidentified packet received - opcode: " + packet.getOpcode() + ".");
}
}
}
@@ -0,0 +1,42 @@
package org.apollo.net.codec.game;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.util.List;
import org.apollo.net.message.Message;
import org.apollo.net.release.MessageEncoder;
import org.apollo.net.release.Release;
/**
* A {@link MessageToMessageEncoder} which encodes {@link Message}s into {@link GamePacket}s.
*
* @author Graham
*/
public final class GameMessageEncoder extends MessageToMessageEncoder<Message> {
/**
* The current release.
*/
private final Release release;
/**
* Creates the game message encoder with the specified release.
*
* @param release The release.
*/
public GameMessageEncoder(Release release) {
this.release = release;
}
@SuppressWarnings("unchecked")
@Override
protected void encode(ChannelHandlerContext ctx, Message message, List<Object> out) {
MessageEncoder<Message> encoder = (MessageEncoder<Message>) release.getMessageEncoder(message.getClass());
if (encoder != null) {
out.add(encoder.encode(message));
}
}
}
@@ -0,0 +1,84 @@
package org.apollo.net.codec.game;
import io.netty.buffer.ByteBuf;
import org.apollo.net.meta.PacketType;
/**
* Represents a single packet used in the in-game protocol.
*
* @author Graham
*/
public final class GamePacket {
/**
* The length.
*/
private final int length;
/**
* The opcode.
*/
private final int opcode;
/**
* The payload.
*/
private final ByteBuf payload;
/**
* The packet type.
*/
private final PacketType type;
/**
* Creates the game packet.
*
* @param opcode The opcode.
* @param type The packet type.
* @param payload The payload.
*/
public GamePacket(int opcode, PacketType type, ByteBuf payload) {
this.opcode = opcode;
this.type = type;
length = payload.readableBytes();
this.payload = payload;
}
/**
* Gets the payload length.
*
* @return The payload length.
*/
public int getLength() {
return length;
}
/**
* Gets the opcode.
*
* @return The opcode.
*/
public int getOpcode() {
return opcode;
}
/**
* Gets the payload.
*
* @return The payload.
*/
public ByteBuf getPayload() {
return payload;
}
/**
* Gets the packet type.
*
* @return The packet type.
*/
public PacketType getType() {
return type;
}
}
@@ -0,0 +1,451 @@
package org.apollo.net.codec.game;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apollo.net.meta.PacketType;
import org.apollo.util.BufferUtil;
/**
* A class which assists in creating a {@link GamePacket}.
*
* @author Graham
*/
public final class GamePacketBuilder {
/**
* The current bit index.
*/
private int bitIndex;
/**
* The buffer.
*/
private final ByteBuf buffer = Unpooled.buffer();
/**
* The current mode.
*/
private AccessMode mode = AccessMode.BYTE_ACCESS;
/**
* The opcode.
*/
private final int opcode;
/**
* The {@link PacketType}.
*/
private final PacketType type;
/**
* Creates a raw {@link GamePacketBuilder}.
*/
public GamePacketBuilder() {
opcode = -1;
type = PacketType.RAW;
}
/**
* Creates the {@link GamePacketBuilder} for a {@link PacketType#FIXED} packet with the specified opcode.
*
* @param opcode The opcode.
*/
public GamePacketBuilder(int opcode) {
this(opcode, PacketType.FIXED);
}
/**
* Creates the {@link GamePacketBuilder} for the specified packet type and opcode.
*
* @param opcode The opcode.
* @param type The packet type.
*/
public GamePacketBuilder(int opcode, PacketType type) {
this.opcode = opcode;
this.type = type;
}
/**
* Checks that this builder is in the bit access mode.
*
* @throws IllegalStateException If the builder is not in bit access mode.
*/
private void checkBitAccess() {
if (mode != AccessMode.BIT_ACCESS) {
throw new IllegalStateException("For bit-based calls to work, the mode must be bit access.");
}
}
/**
* Checks that this builder is in the byte access mode.
*
* @throws IllegalStateException If the builder is not in byte access mode.
*/
private void checkByteAccess() {
if (mode != AccessMode.BYTE_ACCESS) {
throw new IllegalStateException("For byte-based calls to work, the mode must be byte access.");
}
}
/**
* Gets the current length of the builder's buffer.
*
* @return The length of the buffer.
*/
public int getLength() {
checkByteAccess();
return buffer.writerIndex();
}
/**
* Puts a standard data type with the specified value, byte order and transformation.
*
* @param type The data type.
* @param order The byte order.
* @param transformation The transformation.
* @param value The value.
* @throws IllegalArgumentException If the type, order, or transformation is unknown.
*/
public void put(DataType type, DataOrder order, DataTransformation transformation, Number value) {
checkByteAccess();
long longValue = value.longValue();
int length = type.getBytes();
if (order == DataOrder.BIG) {
for (int i = length - 1; i >= 0; i--) {
if (i == 0 && transformation != DataTransformation.NONE) {
if (transformation == DataTransformation.ADD) {
buffer.writeByte((byte) (longValue + 128));
} else if (transformation == DataTransformation.NEGATE) {
buffer.writeByte((byte) -longValue);
} else if (transformation == DataTransformation.SUBTRACT) {
buffer.writeByte((byte) (128 - longValue));
} else {
throw new IllegalArgumentException("Unknown transformation.");
}
} else {
buffer.writeByte((byte) (longValue >> i * 8));
}
}
} else if (order == DataOrder.LITTLE) {
for (int i = 0; i < length; i++) {
if (i == 0 && transformation != DataTransformation.NONE) {
if (transformation == DataTransformation.ADD) {
buffer.writeByte((byte) (longValue + 128));
} else if (transformation == DataTransformation.NEGATE) {
buffer.writeByte((byte) -longValue);
} else if (transformation == DataTransformation.SUBTRACT) {
buffer.writeByte((byte) (128 - longValue));
} else {
throw new IllegalArgumentException("Unknown transformation.");
}
} else {
buffer.writeByte((byte) (longValue >> i * 8));
}
}
} else if (order == DataOrder.MIDDLE) {
if (transformation != DataTransformation.NONE) {
throw new IllegalArgumentException("Middle endian cannot be transformed.");
}
if (type != DataType.INT) {
throw new IllegalArgumentException("Middle endian can only be used with an integer,");
}
buffer.writeByte((byte) (longValue >> 8));
buffer.writeByte((byte) longValue);
buffer.writeByte((byte) (longValue >> 24));
buffer.writeByte((byte) (longValue >> 16));
} else if (order == DataOrder.INVERSED_MIDDLE) {
if (transformation != DataTransformation.NONE) {
throw new IllegalArgumentException("Inversed middle endian cannot be transformed,");
}
if (type != DataType.INT) {
throw new IllegalArgumentException("Inversed middle endian can only be used with an integer,");
}
buffer.writeByte((byte) (longValue >> 16));
buffer.writeByte((byte) (longValue >> 24));
buffer.writeByte((byte) longValue);
buffer.writeByte((byte) (longValue >> 8));
} else {
throw new IllegalArgumentException("Unknown order.");
}
}
/**
* Puts a standard data type with the specified value and byte order.
*
* @param type The data type.
* @param order The byte order.
* @param value The value.
*/
public void put(DataType type, DataOrder order, Number value) {
put(type, order, DataTransformation.NONE, value);
}
/**
* Puts a standard data type with the specified value and transformation.
*
* @param type The type.
* @param transformation The transformation.
* @param value The value.
*/
public void put(DataType type, DataTransformation transformation, Number value) {
put(type, DataOrder.BIG, transformation, value);
}
/**
* Puts a standard data type with the specified value.
*
* @param type The data type.
* @param value The value.
*/
public void put(DataType type, Number value) {
put(type, DataOrder.BIG, DataTransformation.NONE, value);
}
/**
* Puts a single bit into the buffer. If {@code flag} is {@code true}, the value of the bit is {@code 1}. If
* {@code flag} is {@code false}, the value of the bit is {@code 0}.
*
* @param flag The flag.
*/
public void putBit(boolean flag) {
putBit(flag ? 1 : 0);
}
/**
* Puts a single bit into the buffer with the value {@code value}.
*
* @param value The value.
*/
public void putBit(int value) {
putBits(1, value);
}
/**
* Puts {@code numBits} into the buffer with the value {@code value}.
*
* @param numBits The number of bits to put into the buffer.
* @param value The value.
* @throws IllegalArgumentException If the number of bits is not between 1 and 31 inclusive.
*/
public void putBits(int numBits, int value) {
if (numBits < 0 || numBits > 32) {
throw new IllegalArgumentException("Number of bits must be between 1 and 32 inclusive.");
}
checkBitAccess();
int bytePos = bitIndex >> 3;
int bitOffset = 8 - (bitIndex & 7);
bitIndex += numBits;
int requiredSpace = bytePos - buffer.writerIndex() + 1;
requiredSpace += (numBits + 7) / 8;
buffer.ensureWritable(requiredSpace);
for (; numBits > bitOffset; bitOffset = 8) {
int tmp = buffer.getByte(bytePos);
tmp &= ~DataConstants.BIT_MASK[bitOffset];
tmp |= value >> numBits - bitOffset & DataConstants.BIT_MASK[bitOffset];
buffer.setByte(bytePos++, tmp);
numBits -= bitOffset;
}
if (numBits == bitOffset) {
int tmp = buffer.getByte(bytePos);
tmp &= ~DataConstants.BIT_MASK[bitOffset];
tmp |= value & DataConstants.BIT_MASK[bitOffset];
buffer.setByte(bytePos, tmp);
} else {
int tmp = buffer.getByte(bytePos);
tmp &= ~(DataConstants.BIT_MASK[numBits] << bitOffset - numBits);
tmp |= (value & DataConstants.BIT_MASK[numBits]) << bitOffset - numBits;
buffer.setByte(bytePos, tmp);
}
}
/**
* Puts the specified byte array into the buffer.
*
* @param bytes The byte array.
*/
public void putBytes(byte[] bytes) {
buffer.writeBytes(bytes);
}
/**
* Puts the bytes from the specified buffer into this packet's buffer.
*
* @param buffer The source {@link ByteBuf}.
*/
public void putBytes(ByteBuf buffer) {
byte[] bytes = new byte[buffer.readableBytes()];
buffer.markReaderIndex();
try {
buffer.readBytes(bytes);
} finally {
buffer.resetReaderIndex();
}
putBytes(bytes);
}
/**
* Puts the bytes into the buffer with the specified transformation.
*
* @param transformation The transformation.
* @param bytes The byte array.
*/
public void putBytes(DataTransformation transformation, byte[] bytes) {
if (transformation == DataTransformation.NONE) {
putBytes(bytes);
} else {
for (byte b : bytes) {
put(DataType.BYTE, transformation, b);
}
}
}
/**
* Puts the specified byte array into the buffer in reverse.
*
* @param bytes The byte array.
*/
public void putBytesReverse(byte[] bytes) {
checkByteAccess();
for (int i = bytes.length - 1; i >= 0; i--) {
buffer.writeByte(bytes[i]);
}
}
/**
* Puts the bytes from the specified buffer into this packet's buffer, in reverse.
*
* @param buffer The source {@link ByteBuf}.
*/
public void putBytesReverse(ByteBuf buffer) {
byte[] bytes = new byte[buffer.readableBytes()];
buffer.markReaderIndex();
try {
buffer.readBytes(bytes);
} finally {
buffer.resetReaderIndex();
}
putBytesReverse(bytes);
}
/**
* Puts the specified byte array into the buffer in reverse with the specified transformation.
*
* @param transformation The transformation.
* @param bytes The byte array.
*/
public void putBytesReverse(DataTransformation transformation, byte[] bytes) {
if (transformation == DataTransformation.NONE) {
putBytesReverse(bytes);
} else {
for (int i = bytes.length - 1; i >= 0; i--) {
put(DataType.BYTE, transformation, bytes[i]);
}
}
}
/**
* Puts a raw builder. Both builders (this and parameter) must be in byte access mode.
*
* @param builder The builder.
* @throws IllegalArgumentException If the builder is not raw.
*/
public void putRawBuilder(GamePacketBuilder builder) {
checkByteAccess();
if (builder.type != PacketType.RAW) {
throw new IllegalArgumentException("Builder must be raw.");
}
builder.checkByteAccess();
putBytes(builder.buffer);
}
/**
* Puts a raw builder in reverse. Both builders (this and parameter) must be in byte access mode.
*
* @param builder The builder.
* @throws IllegalArgumentException If the builder is not raw.
*/
public void putRawBuilderReverse(GamePacketBuilder builder) {
checkByteAccess();
if (builder.type != PacketType.RAW) {
throw new IllegalArgumentException("Builder must be raw.");
}
builder.checkByteAccess();
putBytesReverse(builder.buffer);
}
/**
* Puts a smart into the buffer.
*
* @param value The value.
*/
public void putSmart(int value) {
checkByteAccess();
if (value < 128) {
buffer.writeByte(value);
} else {
buffer.writeShort(value);
}
}
/**
* Puts a string into the buffer.
*
* @param str The string.
*/
public void putString(String str) {
checkByteAccess();
char[] chars = str.toCharArray();
for (char c : chars) {
buffer.writeByte((byte) c);
}
buffer.writeByte(BufferUtil.STRING_TERMINATOR);
}
/**
* Switches this builder's mode to the bit access mode.
*
* @throws IllegalStateException If the builder is already in bit access mode.
*/
public void switchToBitAccess() {
if (mode == AccessMode.BIT_ACCESS) {
throw new IllegalStateException("Already in bit access mode.");
}
mode = AccessMode.BIT_ACCESS;
bitIndex = buffer.writerIndex() * 8;
}
/**
* Switches this builder's mode to the byte access mode.
*
* @throws IllegalStateException If the builder is already in byte access mode.
*/
public void switchToByteAccess() {
if (mode == AccessMode.BYTE_ACCESS) {
throw new IllegalStateException("Already in byte access mode.");
}
mode = AccessMode.BYTE_ACCESS;
buffer.writerIndex((bitIndex + 7) / 8);
}
/**
* Creates a {@link GamePacket} based on the current contents of this builder.
*
* @return The {@link GamePacket}.
* @throws IllegalStateException If the builder is not in byte access mode, or if the packet is raw.
*/
public GamePacket toGamePacket() {
if (type == PacketType.RAW) {
throw new IllegalStateException("Raw packets cannot be converted to a game packet.");
}
if (mode != AccessMode.BYTE_ACCESS) {
throw new IllegalStateException("Must be in byte access mode to convert to a packet.");
}
return new GamePacket(opcode, type, buffer);
}
}
@@ -0,0 +1,140 @@
package org.apollo.net.codec.game;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import java.util.List;
import org.apollo.net.meta.PacketMetaData;
import org.apollo.net.meta.PacketType;
import org.apollo.net.release.Release;
import org.apollo.util.StatefulFrameDecoder;
import org.apollo.util.security.IsaacRandom;
import com.google.common.base.Preconditions;
/**
* A {@link StatefulFrameDecoder} that decodes {@link GamePacket}s.
*
* @author Graham
*/
public final class GamePacketDecoder extends StatefulFrameDecoder<GameDecoderState> {
/**
* The current length.
*/
private int length;
/**
* The current opcode.
*/
private int opcode;
/**
* The random number generator.
*/
private final IsaacRandom random;
/**
* The current release.
*/
private final Release release;
/**
* The packet type.
*/
private PacketType type;
/**
* Creates the {@link GamePacketDecoder}.
*
* @param random The random number generator.
* @param release The current release.
*/
public GamePacketDecoder(IsaacRandom random, Release release) {
super(GameDecoderState.GAME_OPCODE);
this.random = random;
this.release = release;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out, GameDecoderState state) {
switch (state) {
case GAME_OPCODE:
decodeOpcode(in, out);
break;
case GAME_LENGTH:
decodeLength(in);
break;
case GAME_PAYLOAD:
decodePayload(in, out);
break;
default:
throw new IllegalStateException("Invalid game decoder state.");
}
}
/**
* Decodes the length state.
*
* @param buffer The buffer.
*/
private void decodeLength(ByteBuf buffer) {
if (buffer.isReadable()) {
length = buffer.readUnsignedByte();
if (length != 0) {
setState(GameDecoderState.GAME_PAYLOAD);
}
}
}
/**
* Decodes the opcode state.
*
* @param buffer The buffer.
* @param out The {@link List} of objects to be passed along the pipeline.
*/
private void decodeOpcode(ByteBuf buffer, List<Object> out) {
if (buffer.isReadable()) {
int encryptedOpcode = buffer.readUnsignedByte();
opcode = encryptedOpcode - random.nextInt() & 0xFF;
PacketMetaData metaData = release.getIncomingPacketMetaData(opcode);
Preconditions.checkNotNull(metaData, "Illegal opcode: " + opcode + ".");
type = metaData.getType();
switch (type) {
case FIXED:
length = metaData.getLength();
if (length == 0) {
setState(GameDecoderState.GAME_OPCODE);
out.add(new GamePacket(opcode, type, Unpooled.EMPTY_BUFFER));
} else {
setState(GameDecoderState.GAME_PAYLOAD);
}
break;
case VARIABLE_BYTE:
setState(GameDecoderState.GAME_LENGTH);
break;
default:
throw new IllegalStateException("Illegal packet type: " + type + ".");
}
}
}
/**
* Decodes the payload state.
*
* @param buffer The buffer.
* @param out The {@link List} of objects to be passed along the pipeline.
*/
private void decodePayload(ByteBuf buffer, List<Object> out) {
if (buffer.readableBytes() >= length) {
ByteBuf payload = buffer.readBytes(length);
setState(GameDecoderState.GAME_OPCODE);
out.add(new GamePacket(opcode, type, payload));
}
}
}
@@ -0,0 +1,63 @@
package org.apollo.net.codec.game;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.util.List;
import org.apollo.net.meta.PacketType;
import org.apollo.util.security.IsaacRandom;
/**
* A {@link MessageToMessageEncoder} which encodes in-game packets.
*
* @author Graham
*/
public final class GamePacketEncoder extends MessageToMessageEncoder<GamePacket> {
/**
* The random number generator.
*/
private final IsaacRandom random;
/**
* Creates the {@link GamePacketEncoder}.
*
* @param random The random number generator.
*/
public GamePacketEncoder(IsaacRandom random) {
this.random = random;
}
@Override
protected void encode(ChannelHandlerContext ctx, GamePacket packet, List<Object> out) throws Exception {
PacketType type = packet.getType();
int headerLength = 1;
int payloadLength = packet.getLength();
if (type == PacketType.VARIABLE_BYTE) {
headerLength++;
if (payloadLength >= 256) {
throw new Exception("Payload too long for variable byte packet.");
}
} else if (type == PacketType.VARIABLE_SHORT) {
headerLength += 2;
if (payloadLength >= 65_536) {
throw new Exception("Payload too long for variable short packet.");
}
}
ByteBuf buffer = Unpooled.buffer(headerLength + payloadLength);
buffer.writeByte(packet.getOpcode() + random.nextInt() & 0xFF);
if (type == PacketType.VARIABLE_BYTE) {
buffer.writeByte(payloadLength);
} else if (type == PacketType.VARIABLE_SHORT) {
buffer.writeShort(payloadLength);
}
buffer.writeBytes(packet.getPayload());
out.add(buffer);
}
}
@@ -0,0 +1,417 @@
package org.apollo.net.codec.game;
import io.netty.buffer.ByteBuf;
import org.apollo.util.BufferUtil;
import com.google.common.base.Preconditions;
/**
* A utility class for reading {@link GamePacket}s.
*
* @author Graham
*/
public final class GamePacketReader {
/**
* The current bit index.
*/
private int bitIndex;
/**
* The buffer.
*/
private final ByteBuf buffer;
/**
* The current mode.
*/
private AccessMode mode = AccessMode.BYTE_ACCESS;
/**
* Creates the reader.
*
* @param packet The packet.
*/
public GamePacketReader(GamePacket packet) {
buffer = packet.getPayload();
}
/**
* Checks that this reader is in the bit access mode.
*
* @throws IllegalStateException If the reader is not in bit access mode.
*/
private void checkBitAccess() {
Preconditions.checkState(mode == AccessMode.BIT_ACCESS,
"For bit-based calls to work, the mode must be bit access.");
}
/**
* Checks that this reader is in the byte access mode.
*
* @throws IllegalStateException If the reader is not in byte access mode.
*/
private void checkByteAccess() {
Preconditions.checkState(mode == AccessMode.BYTE_ACCESS,
"For byte-based calls to work, the mode must be byte access.");
}
/**
* Reads a standard data type from the buffer with the specified order and transformation.
*
* @param type The data type.
* @param order The data order.
* @param transformation The data transformation.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
* @throws IllegalArgumentException If the combination is invalid.
*/
private long get(DataType type, DataOrder order, DataTransformation transformation) {
checkByteAccess();
long longValue = 0;
int length = type.getBytes();
if (order == DataOrder.BIG) {
for (int i = length - 1; i >= 0; i--) {
if (i == 0 && transformation != DataTransformation.NONE) {
if (transformation == DataTransformation.ADD) {
longValue |= buffer.readByte() - 128 & 0xFFL;
} else if (transformation == DataTransformation.NEGATE) {
longValue |= -buffer.readByte() & 0xFFL;
} else if (transformation == DataTransformation.SUBTRACT) {
longValue |= 128 - buffer.readByte() & 0xFFL;
} else {
throw new IllegalArgumentException("Unknown transformation.");
}
} else {
longValue |= (buffer.readByte() & 0xFFL) << i * 8;
}
}
} else if (order == DataOrder.LITTLE) {
for (int i = 0; i < length; i++) {
if (i == 0 && transformation != DataTransformation.NONE) {
if (transformation == DataTransformation.ADD) {
longValue |= buffer.readByte() - 128 & 0xFFL;
} else if (transformation == DataTransformation.NEGATE) {
longValue |= -buffer.readByte() & 0xFFL;
} else if (transformation == DataTransformation.SUBTRACT) {
longValue |= 128 - buffer.readByte() & 0xFFL;
} else {
throw new IllegalArgumentException("Unknown transformation.");
}
} else {
longValue |= (buffer.readByte() & 0xFFL) << i * 8;
}
}
} else if (order == DataOrder.MIDDLE) {
if (transformation != DataTransformation.NONE) {
throw new IllegalArgumentException("Middle endian cannot be transformed.");
}
if (type != DataType.INT) {
throw new IllegalArgumentException("Middle endian can only be used with an integer.");
}
longValue |= (buffer.readByte() & 0xFF) << 8;
longValue |= buffer.readByte() & 0xFF;
longValue |= (buffer.readByte() & 0xFF) << 24;
longValue |= (buffer.readByte() & 0xFF) << 16;
} else if (order == DataOrder.INVERSED_MIDDLE) {
if (transformation != DataTransformation.NONE) {
throw new IllegalArgumentException("Inversed middle endian cannot be transformed.");
}
if (type != DataType.INT) {
throw new IllegalArgumentException("Inversed middle endian can only be used with an integer.");
}
longValue |= (buffer.readByte() & 0xFF) << 16;
longValue |= (buffer.readByte() & 0xFF) << 24;
longValue |= buffer.readByte() & 0xFF;
longValue |= (buffer.readByte() & 0xFF) << 8;
} else {
throw new IllegalArgumentException("Unknown order.");
}
return longValue;
}
/**
* Gets a bit from the buffer.
*
* @return The value.
* @throws IllegalStateException If the reader is not in bit access mode.
*/
public int getBit() {
return getBits(1);
}
/**
* Gets the specified amount of bits from the buffer.
*
* @param amount The amount of bits.
* @return The value.
* @throws IllegalStateException If the reader is not in bit access mode.
* @throws IllegalArgumentException If the number of bits is not between 1 and 31 inclusive.
*/
public int getBits(int amount) {
Preconditions.checkArgument(amount >= 0 && amount <= 32, "Number of bits must be between 1 and 32 inclusive.");
checkBitAccess();
int bytePos = bitIndex >> 3;
int bitOffset = 8 - (bitIndex & 7);
int value = 0;
bitIndex += amount;
for (; amount > bitOffset; bitOffset = 8) {
value += (buffer.getByte(bytePos++) & DataConstants.BIT_MASK[bitOffset]) << amount - bitOffset;
amount -= bitOffset;
}
if (amount == bitOffset) {
value += buffer.getByte(bytePos) & DataConstants.BIT_MASK[bitOffset];
} else {
value += buffer.getByte(bytePos) >> bitOffset - amount & DataConstants.BIT_MASK[amount];
}
return value;
}
/**
* Gets bytes.
*
* @param bytes The target byte array.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public void getBytes(byte[] bytes) {
checkByteAccess();
for (int i = 0; i < bytes.length; i++) {
bytes[i] = buffer.readByte();
}
}
/**
* Gets bytes with the specified transformation.
*
* @param transformation The transformation.
* @param bytes The target byte array.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public void getBytes(DataTransformation transformation, byte[] bytes) {
if (transformation == DataTransformation.NONE) {
getBytesReverse(bytes);
} else {
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) getSigned(DataType.BYTE, transformation);
}
}
}
/**
* Gets bytes in reverse.
*
* @param bytes The target byte array.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public void getBytesReverse(byte[] bytes) {
checkByteAccess();
for (int i = bytes.length - 1; i >= 0; i--) {
bytes[i] = buffer.readByte();
}
}
/**
* Gets bytes in reverse with the specified transformation.
*
* @param transformation The transformation.
* @param bytes The target byte array.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public void getBytesReverse(DataTransformation transformation, byte[] bytes) {
if (transformation == DataTransformation.NONE) {
getBytesReverse(bytes);
} else {
for (int i = bytes.length - 1; i >= 0; i--) {
bytes[i] = (byte) getSigned(DataType.BYTE, transformation);
}
}
}
/**
* Gets the length of this reader.
*
* @return The length of this reader.
*/
public int getLength() {
checkByteAccess();
return buffer.writableBytes();
}
/**
* Gets a signed data type from the buffer.
*
* @param type The data type.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public long getSigned(DataType type) {
return getSigned(type, DataOrder.BIG, DataTransformation.NONE);
}
/**
* Gets a signed data type from the buffer with the specified order.
*
* @param type The data type.
* @param order The byte order.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
* @throws IllegalArgumentException If the combination is invalid.
*/
public long getSigned(DataType type, DataOrder order) {
return getSigned(type, order, DataTransformation.NONE);
}
/**
* Gets a signed data type from the buffer with the specified order and transformation.
*
* @param type The data type.
* @param order The byte order.
* @param transformation The data transformation.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
* @throws IllegalArgumentException If the combination is invalid.
*/
public long getSigned(DataType type, DataOrder order, DataTransformation transformation) {
long longValue = get(type, order, transformation);
if (type != DataType.LONG) {
int max = (int) (Math.pow(2, type.getBytes() * 8 - 1) - 1);
if (longValue > max) {
longValue -= (max + 1) * 2;
}
}
return longValue;
}
/**
* Gets a signed data type from the buffer with the specified transformation.
*
* @param type The data type.
* @param transformation The data transformation.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
* @throws IllegalArgumentException If the combination is invalid.
*/
public long getSigned(DataType type, DataTransformation transformation) {
return getSigned(type, DataOrder.BIG, transformation);
}
/**
* Gets a signed smart from the buffer.
*
* @return The smart.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public int getSignedSmart() {
checkByteAccess();
int peek = buffer.getByte(buffer.readerIndex());
if (peek < 128) {
return buffer.readByte() - 64;
}
return buffer.readShort() - 49152;
}
/**
* Gets a string from the buffer.
*
* @return The string.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public String getString() {
checkByteAccess();
return BufferUtil.readString(buffer);
}
/**
* Gets an unsigned data type from the buffer.
*
* @param type The data type.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public long getUnsigned(DataType type) {
return getUnsigned(type, DataOrder.BIG, DataTransformation.NONE);
}
/**
* Gets an unsigned data type from the buffer with the specified order.
*
* @param type The data type.
* @param order The byte order.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
* @throws IllegalArgumentException If the combination is invalid.
*/
public long getUnsigned(DataType type, DataOrder order) {
return getUnsigned(type, order, DataTransformation.NONE);
}
/**
* Gets an unsigned data type from the buffer with the specified order and transformation.
*
* @param type The data type.
* @param order The byte order.
* @param transformation The data transformation.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
* @throws IllegalArgumentException If the combination is invalid.
*/
public long getUnsigned(DataType type, DataOrder order, DataTransformation transformation) {
long longValue = get(type, order, transformation);
Preconditions.checkArgument(type != DataType.LONG, "Longs must be read as a signed type.");
return longValue & 0xFFFFFFFFFFFFFFFFL;
}
/**
* Gets an unsigned data type from the buffer with the specified transformation.
*
* @param type The data type.
* @param transformation The data transformation.
* @return The value.
* @throws IllegalStateException If this reader is not in byte access mode.
* @throws IllegalArgumentException If the combination is invalid.
*/
public long getUnsigned(DataType type, DataTransformation transformation) {
return getUnsigned(type, DataOrder.BIG, transformation);
}
/**
* Gets an unsigned smart from the buffer.
*
* @return The smart.
* @throws IllegalStateException If this reader is not in byte access mode.
*/
public int getUnsignedSmart() {
checkByteAccess();
int peek = buffer.getByte(buffer.readerIndex());
if (peek < 128) {
return buffer.readByte();
}
return buffer.readShort() - 32768;
}
/**
* Switches this builder's mode to the bit access mode.
*
* @throws IllegalStateException If the builder is already in bit access mode.
*/
public void switchToBitAccess() {
Preconditions.checkState(mode != AccessMode.BIT_ACCESS, "Already in bit access mode.");
mode = AccessMode.BIT_ACCESS;
bitIndex = buffer.readerIndex() * 8;
}
/**
* Switches this builder's mode to the byte access mode.
*
* @throws IllegalStateException If the builder is already in byte access mode.
*/
public void switchToByteAccess() {
Preconditions.checkState(mode != AccessMode.BYTE_ACCESS, "Already in byte access mode.");
mode = AccessMode.BYTE_ACCESS;
buffer.readerIndex((bitIndex + 7) / 8);
}
}
@@ -0,0 +1,4 @@
/**
* Contains codecs for the game protocol.
*/
package org.apollo.net.codec.game;
@@ -0,0 +1,27 @@
package org.apollo.net.codec.handshake;
/**
* Holds handshake-related constants.
*
* @author Graham
*/
public final class HandshakeConstants {
/**
* The id of the game service.
*/
public static final int SERVICE_GAME = 14;
/**
* The id of the update service.
*/
public static final int SERVICE_UPDATE = 15;
/**
* Default private constructor to prevent instantiation by other classes.
*/
private HandshakeConstants() {
}
}
@@ -0,0 +1,60 @@
package org.apollo.net.codec.handshake;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
import java.util.logging.Logger;
import org.apollo.net.codec.login.LoginDecoder;
import org.apollo.net.codec.login.LoginEncoder;
import org.apollo.net.codec.update.UpdateDecoder;
import org.apollo.net.codec.update.UpdateEncoder;
/**
* A {@link ByteToMessageDecoder} which decodes the handshake and makes changes to the pipeline as appropriate for the
* selected service.
*
* @author Graham
*/
public final class HandshakeDecoder extends ByteToMessageDecoder {
/**
* The logger for this class.
*/
private static final Logger logger = Logger.getLogger(HandshakeDecoder.class.getName());
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) {
if (!buffer.isReadable()) {
return;
}
int id = buffer.readUnsignedByte();
switch (id) {
case HandshakeConstants.SERVICE_GAME:
ctx.pipeline().addFirst("loginEncoder", new LoginEncoder());
ctx.pipeline().addAfter("handshakeDecoder", "loginDecoder", new LoginDecoder());
break;
case HandshakeConstants.SERVICE_UPDATE:
ctx.pipeline().addFirst("updateEncoder", new UpdateEncoder());
ctx.pipeline().addBefore("handler", "updateDecoder", new UpdateDecoder());
ByteBuf buf = ctx.alloc().buffer(8).writeLong(0);
ctx.channel().writeAndFlush(buf);
break;
default:
ByteBuf data = buffer.readBytes(buffer.readableBytes());
logger.info(String.format("Unexpected handshake request received: %d data: %s", id, data.toString()));
return;
}
ctx.pipeline().remove(this);
out.add(new HandshakeMessage(id));
}
}
@@ -0,0 +1,33 @@
package org.apollo.net.codec.handshake;
/**
* A handshake message in the service handshake protocol.
*
* @author Graham
*/
public final class HandshakeMessage {
/**
* The service id.
*/
private final int serviceId;
/**
* Creates the handshake message.
*
* @param serviceId The service id.
*/
public HandshakeMessage(int serviceId) {
this.serviceId = serviceId;
}
/**
* Gets the service id.
*
* @return The service id.
*/
public int getServiceId() {
return serviceId;
}
}
@@ -0,0 +1,4 @@
/**
* Contains codecs for the handshake protocol.
*/
package org.apollo.net.codec.handshake;
@@ -0,0 +1,33 @@
package org.apollo.net.codec.jaggrab;
/**
* Represents the request for a single file using the JAGGRAB protocol.
*
* @author Graham
*/
public final class JagGrabRequest {
/**
* The path to the file.
*/
private final String filePath;
/**
* Creates the request.
*
* @param filePath The file path.
*/
public JagGrabRequest(String filePath) {
this.filePath = filePath;
}
/**
* Gets the file path.
*
* @return The file path.
*/
public String getFilePath() {
return filePath;
}
}
@@ -0,0 +1,25 @@
package org.apollo.net.codec.jaggrab;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
/**
* A {@link MessageToMessageDecoder} for the JAGGRAB protocol.
*
* @author Graham
*/
public final class JagGrabRequestDecoder extends MessageToMessageDecoder<String> {
@Override
protected void decode(ChannelHandlerContext ctx, String request, List<Object> out) {
if (request.startsWith("JAGGRAB /")) {
String filePath = request.substring(8).trim();
out.add(new JagGrabRequest(filePath));
} else {
throw new IllegalArgumentException("Corrupted request line.");
}
}
}
@@ -0,0 +1,35 @@
package org.apollo.net.codec.jaggrab;
import io.netty.buffer.ByteBuf;
/**
* Represents a single JAGGRAB response.
*
* @author Graham
*/
public final class JagGrabResponse {
/**
* The file data.
*/
private final ByteBuf fileData;
/**
* Creates the response.
*
* @param fileData The file data.
*/
public JagGrabResponse(ByteBuf fileData) {
this.fileData = fileData;
}
/**
* Gets the file data.
*
* @return The file data.
*/
public ByteBuf getFileData() {
return fileData;
}
}
@@ -0,0 +1,20 @@
package org.apollo.net.codec.jaggrab;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.util.List;
/**
* A {@link MessageToMessageEncoder} for the JAGGRAB protocol.
*
* @author Graham
*/
public final class JagGrabResponseEncoder extends MessageToMessageEncoder<JagGrabResponse> {
@Override
protected void encode(ChannelHandlerContext ctx, JagGrabResponse response, List<Object> out) {
out.add(response.getFileData());
}
}
@@ -0,0 +1,4 @@
/**
* Contains codecs for the JAGGRAB protocol.
*/
package org.apollo.net.codec.jaggrab;
@@ -0,0 +1,127 @@
package org.apollo.net.codec.login;
/**
* Holds login-related constants.
*
* @author Graham
*/
public final class LoginConstants {
/**
* Exchange data login status.
*/
public static final int STATUS_EXCHANGE_DATA = 0;
/**
* Delay for 2 seconds login status.
*/
public static final int STATUS_DELAY = 1;
/**
* OK login status.
*/
public static final int STATUS_OK = 2;
/**
* Invalid credentials login status.
*/
public static final int STATUS_INVALID_CREDENTIALS = 3;
/**
* Account disabled login status.
*/
public static final int STATUS_ACCOUNT_DISABLED = 4;
/**
* Account online login status.
*/
public static final int STATUS_ACCOUNT_ONLINE = 5;
/**
* Game updated login status.
*/
public static final int STATUS_GAME_UPDATED = 6;
/**
* Server full login status.
*/
public static final int STATUS_SERVER_FULL = 7;
/**
* Login server offline login status.
*/
public static final int STATUS_LOGIN_SERVER_OFFLINE = 8;
/**
* Too many connections login status.
*/
public static final int STATUS_TOO_MANY_CONNECTIONS = 9;
/**
* Bad session id login status.
*/
public static final int STATUS_BAD_SESSION_ID = 10;
/**
* Login server rejected session login status.
*/
public static final int STATUS_LOGIN_SERVER_REJECTED_SESSION = 11;
/**
* Members account required login status.
*/
public static final int STATUS_MEMBERS_ACCOUNT_REQUIRED = 12;
/**
* Could not complete login status.
*/
public static final int STATUS_COULD_NOT_COMPLETE = 13;
/**
* Server updating login status.
*/
public static final int STATUS_UPDATING = 14;
/**
* Reconnection OK login status.
*/
public static final int STATUS_RECONNECTION_OK = 15;
/**
* Too many login attempts login status.
*/
public static final int STATUS_TOO_MANY_LOGINS = 16;
/**
* Standing in members area on free world status.
*/
public static final int STATUS_IN_MEMBERS_AREA = 17;
/**
* Invalid login server status.
*/
public static final int STATUS_INVALID_LOGIN_SERVER = 20;
/**
* Profile transfer login status.
*/
public static final int STATUS_PROFILE_TRANSFER = 21;
/**
* Standard login type id.
*/
public static final int TYPE_STANDARD = 16;
/**
* Reconnection login type id.
*/
public static final int TYPE_RECONNECTION = 18;
/**
* Default private constructor to prevent instantiation.
*/
private LoginConstants() {
}
}
@@ -0,0 +1,220 @@
package org.apollo.net.codec.login;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.security.SecureRandom;
import java.util.List;
import org.apollo.cache.FileSystemConstants;
import org.apollo.net.NetworkConstants;
import org.apollo.util.BufferUtil;
import org.apollo.util.StatefulFrameDecoder;
import org.apollo.util.security.IsaacRandom;
import org.apollo.util.security.IsaacRandomPair;
import org.apollo.util.security.PlayerCredentials;
import com.google.common.net.InetAddresses;
/**
* A {@link StatefulFrameDecoder} which decodes the login request frames.
*
* @author Graham
*/
public final class LoginDecoder extends StatefulFrameDecoder<LoginDecoderState> {
/**
* The secure random number generator.
*/
private static final SecureRandom RANDOM = new SecureRandom();
/**
* The login packet length.
*/
private int loginLength;
/**
* The reconnecting flag.
*/
private boolean reconnecting;
/**
* The server-side session key.
*/
private long serverSeed;
/**
* The username hash.
*/
private int usernameHash;
/**
* Creates the login decoder with the default initial state.
*/
public LoginDecoder() {
super(LoginDecoderState.LOGIN_HANDSHAKE);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out, LoginDecoderState state) {
switch (state) {
case LOGIN_HANDSHAKE:
decodeHandshake(ctx, in, out);
break;
case LOGIN_HEADER:
decodeHeader(ctx, in, out);
break;
case LOGIN_PAYLOAD:
decodePayload(ctx, in, out);
break;
default:
throw new IllegalStateException("Invalid login decoder state: " + state);
}
}
/**
* Decodes in the handshake state.
*
* @param ctx The channel handler context.
* @param buffer The buffer.
* @param out The {@link List} of objects to pass forward through the pipeline.
*/
private void decodeHandshake(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) {
if (buffer.isReadable()) {
usernameHash = buffer.readUnsignedByte();
serverSeed = RANDOM.nextLong();
ByteBuf response = ctx.alloc().buffer(17);
response.writeByte(LoginConstants.STATUS_EXCHANGE_DATA);
response.writeLong(0);
response.writeLong(serverSeed);
ctx.channel().write(response);
setState(LoginDecoderState.LOGIN_HEADER);
}
}
/**
* Decodes in the header state.
*
* @param ctx The channel handler context.
* @param buffer The buffer.
* @param out The {@link List} of objects to pass forward through the pipeline.
*/
private void decodeHeader(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) {
if (buffer.readableBytes() >= 2) {
int type = buffer.readUnsignedByte();
if (type != LoginConstants.TYPE_STANDARD && type != LoginConstants.TYPE_RECONNECTION) {
writeResponseCode(ctx, LoginConstants.STATUS_LOGIN_SERVER_REJECTED_SESSION);
return;
}
reconnecting = type == LoginConstants.TYPE_RECONNECTION;
loginLength = buffer.readUnsignedByte();
setState(LoginDecoderState.LOGIN_PAYLOAD);
}
}
/**
* Decodes in the payload state.
*
* @param ctx The channel handler context.
* @param buffer The buffer.
* @param out The {@link List} of objects to pass forward through the pipeline.
*/
private void decodePayload(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) {
if (buffer.readableBytes() >= loginLength) {
ByteBuf payload = buffer.readBytes(loginLength);
int version = 255 - payload.readUnsignedByte();
int release = payload.readUnsignedShort();
int memoryStatus = payload.readUnsignedByte();
if (memoryStatus != 0 && memoryStatus != 1) {
writeResponseCode(ctx, LoginConstants.STATUS_LOGIN_SERVER_REJECTED_SESSION);
return;
}
boolean lowMemory = memoryStatus == 1;
int[] crcs = new int[FileSystemConstants.ARCHIVE_COUNT];
for (int index = 0; index < 9; index++) {
crcs[index] = payload.readInt();
}
int length = payload.readUnsignedByte();
if (length != loginLength - 41) {
writeResponseCode(ctx, LoginConstants.STATUS_LOGIN_SERVER_REJECTED_SESSION);
return;
}
ByteBuf secure = payload.readBytes(length);
BigInteger value = new BigInteger(secure.array());
value = value.modPow(NetworkConstants.RSA_EXPONENT, NetworkConstants.RSA_MODULUS);
secure = Unpooled.wrappedBuffer(value.toByteArray());
int id = secure.readUnsignedByte();
if (id != 10) {
writeResponseCode(ctx, LoginConstants.STATUS_LOGIN_SERVER_REJECTED_SESSION);
return;
}
long clientSeed = secure.readLong();
long reportedSeed = secure.readLong();
if (reportedSeed != serverSeed) {
writeResponseCode(ctx, LoginConstants.STATUS_LOGIN_SERVER_REJECTED_SESSION);
return;
}
int uid = secure.readInt();
String username = BufferUtil.readString(secure);
String password = BufferUtil.readString(secure);
InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
String hostAddress = InetAddresses.toAddrString(socketAddress.getAddress());
if (password.length() < 6 || password.length() > 20 || username.isEmpty() || username.length() > 12) {
writeResponseCode(ctx, LoginConstants.STATUS_INVALID_CREDENTIALS);
return;
}
int[] seed = new int[4];
seed[0] = (int) (clientSeed >> 32);
seed[1] = (int) clientSeed;
seed[2] = (int) (serverSeed >> 32);
seed[3] = (int) serverSeed;
IsaacRandom decodingRandom = new IsaacRandom(seed);
for (int index = 0; index < seed.length; index++) {
seed[index] += 50;
}
IsaacRandom encodingRandom = new IsaacRandom(seed);
PlayerCredentials credentials = new PlayerCredentials(username, password, usernameHash, uid, hostAddress);
IsaacRandomPair randomPair = new IsaacRandomPair(encodingRandom, decodingRandom);
out.add(new LoginRequest(credentials, randomPair, reconnecting, lowMemory, release, crcs, version));
}
}
/**
* Writes a response code to the client and closes the current channel.
*
* @param ctx The context of the channel handler.
* @param response The response code to write.
*/
private void writeResponseCode(ChannelHandlerContext ctx, int response) {
ByteBuf buffer = ctx.alloc().buffer(Byte.BYTES);
buffer.writeByte(response);
ctx.write(buffer).addListener(ChannelFutureListener.CLOSE);
}
}
@@ -0,0 +1,28 @@
package org.apollo.net.codec.login;
/**
* An enumeration with the different states the {@link LoginDecoder} can be in.
*
* @author Graham
*/
public enum LoginDecoderState {
/**
* The login handshake state will wait for the username hash to be received. Once it is, a server session key will
* be sent to the client and the state will be set to the login header state.
*/
LOGIN_HANDSHAKE,
/**
* The login header state will wait for the login type and payload length to be received. These are saved, and then
* the state will be set to the login payload state.
*/
LOGIN_HEADER,
/**
* The login payload state will wait for all login information (such as client release number, username and
* password).
*/
LOGIN_PAYLOAD;
}
@@ -0,0 +1,36 @@
package org.apollo.net.codec.login;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.util.List;
/**
* A {@link MessageToMessageEncoder} which encodes login response messages.
*
* @author Graham
*/
public final class LoginEncoder extends MessageToMessageEncoder<LoginResponse> {
/**
* Creates the login encoder.
*/
public LoginEncoder() {
super(LoginResponse.class);
}
@Override
protected void encode(ChannelHandlerContext ctx, LoginResponse response, List<Object> out) {
ByteBuf buffer = ctx.alloc().buffer(3);
buffer.writeByte(response.getStatus());
if (response.getStatus() == LoginConstants.STATUS_OK) {
buffer.writeByte(response.getRights());
buffer.writeByte(response.isFlagged() ? 1 : 0);
}
out.add(buffer);
}
}
@@ -0,0 +1,132 @@
package org.apollo.net.codec.login;
import org.apollo.util.security.IsaacRandomPair;
import org.apollo.util.security.PlayerCredentials;
/**
* Represents a login request.
*
* @author Graham
*/
public final class LoginRequest {
/**
* The archive CRCs.
*/
private final int[] archiveCrcs;
/**
* The version denoting whether the client has been modified or not.
*/
private final int clientVersion;
/**
* The player's credentials.
*/
private final PlayerCredentials credentials;
/**
* The low memory flag.
*/
private final boolean lowMemory;
/**
* The pair of random number generators.
*/
private final IsaacRandomPair randomPair;
/**
* The reconnecting flag.
*/
private final boolean reconnecting;
/**
* The release number.
*/
private final int releaseNumber;
/**
* Creates a login request.
*
* @param credentials The player credentials.
* @param randomPair The pair of random number generators.
* @param lowMemory The low memory flag.
* @param reconnecting The reconnecting flag.
* @param releaseNumber The release number.
* @param archiveCrcs The archive CRCs.
* @param clientVersion The client version.
*/
public LoginRequest(PlayerCredentials credentials, IsaacRandomPair randomPair, boolean lowMemory, boolean reconnecting, int releaseNumber, int[] archiveCrcs, int clientVersion) {
this.credentials = credentials;
this.randomPair = randomPair;
this.lowMemory = lowMemory;
this.reconnecting = reconnecting;
this.releaseNumber = releaseNumber;
this.archiveCrcs = archiveCrcs;
this.clientVersion = clientVersion;
}
/**
* Gets the archive CRCs.
*
* @return The array of archive CRCs.
*/
public int[] getArchiveCrcs() {
return archiveCrcs;
}
/**
* Gets the value denoting the client's (modified) version.
*
* @return The client version.
*/
public int getClientVersion() {
return clientVersion;
}
/**
* Gets the player's credentials.
*
* @return The player's credentials.
*/
public PlayerCredentials getCredentials() {
return credentials;
}
/**
* Gets the pair of random number generators.
*
* @return The pair of random number generators.
*/
public IsaacRandomPair getRandomPair() {
return randomPair;
}
/**
* Gets the release number.
*
* @return The release number.
*/
public int getReleaseNumber() {
return releaseNumber;
}
/**
* Checks if this client is in low memory mode.
*
* @return {@code true} if so, {@code false} if not.
*/
public boolean isLowMemory() {
return lowMemory;
}
/**
* Checks if this client is reconnecting.
*
* @return {@code true} if so, {@code false} if not.
*/
public boolean isReconnecting() {
return reconnecting;
}
}
@@ -0,0 +1,65 @@
package org.apollo.net.codec.login;
/**
* Represents a login response.
*
* @author Graham
*/
public final class LoginResponse {
/**
* The flagged flag.
*/
private final boolean flagged;
/**
* The rights level.
*/
private final int rights;
/**
* The login status.
*/
private final int status;
/**
* Creates the login response.
*
* @param status The login status.
* @param rights The rights level.
* @param flagged The flagged flag.
*/
public LoginResponse(int status, int rights, boolean flagged) {
this.status = status;
this.rights = rights;
this.flagged = flagged;
}
/**
* Gets the rights level.
*
* @return The rights level.
*/
public int getRights() {
return rights;
}
/**
* Gets the status.
*
* @return The status.
*/
public int getStatus() {
return status;
}
/**
* Checks if the player should be flagged.
*
* @return The flagged flag.
*/
public boolean isFlagged() {
return flagged;
}
}
@@ -0,0 +1,4 @@
/**
* Contains codecs for the login protocol.
*/
package org.apollo.net.codec.login;
@@ -0,0 +1,134 @@
package org.apollo.net.codec.update;
import org.apollo.cache.FileDescriptor;
/**
* Represents a single 'on-demand' request.
*
* @author Graham
*/
public final class OnDemandRequest implements Comparable<OnDemandRequest> {
/**
* An enumeration containing the different request priorities.
*/
public enum Priority {
/**
* High priority - used when a player is in-game and data is required immediately.
*/
HIGH(0),
/**
* Medium priority - used while loading extra resources when the client is not logged in.
*/
MEDIUM(1),
/**
* Low priority - used when a file is not required urgently (such as when serving the rest of the cache whilst
* the player is in-game).
*/
LOW(2);
/**
* Converts the integer value to a Priority.
*
* @param value The integer value.
* @return The priority.
* @throws IllegalArgumentException If the value is outside of the range 1-3 inclusive.
*/
public static Priority valueOf(int value) {
switch (value) {
case 0:
return HIGH;
case 1:
return MEDIUM;
case 2:
return LOW;
default:
throw new IllegalArgumentException("Priority out of range - received " + value + ".");
}
}
/**
* The integer value.
*/
private final int value;
/**
* Creates the Priority.
*
* @param value The integer value.
*/
private Priority(int value) {
this.value = value;
}
/**
* Compares this Priority with the specified other Priority.
* <p>
* Used as an ordinal-independent variant of {@link #compareTo}.
*
* @param other The other Priority.
* @return 1 if this Priority is greater than {@code other}, 0 if they are equal, otherwise -1.
*/
public int compareWith(Priority other) {
return Integer.compare(value, other.value);
}
/**
* Converts the priority to an integer.
*
* @return The integer value.
*/
public int toInteger() {
return value;
}
}
/**
* The FileDescriptor.
*/
private final FileDescriptor descriptor;
/**
* The request Priority.
*/
private final Priority priority;
/**
* Creates the OnDemandRequest.
*
* @param descriptor The {@link FileDescriptor}.
* @param priority The {@link Priority}.
*/
public OnDemandRequest(FileDescriptor descriptor, Priority priority) {
this.descriptor = descriptor;
this.priority = priority;
}
@Override
public int compareTo(OnDemandRequest other) {
return priority.compareWith(other.priority);
}
/**
* Gets the {@link FileDescriptor}.
*
* @return The FileDescriptor.
*/
public FileDescriptor getFileDescriptor() {
return descriptor;
}
/**
* Gets the {@link Priority}.
*
* @return The Priority.
*/
public Priority getPriority() {
return priority;
}
}
@@ -0,0 +1,85 @@
package org.apollo.net.codec.update;
import io.netty.buffer.ByteBuf;
import org.apollo.cache.FileDescriptor;
/**
* Represents a single 'on-demand' response.
*
* @author Graham
*/
public final class OnDemandResponse {
/**
* The chunk data.
*/
private final ByteBuf chunkData;
/**
* The chunk id.
*/
private final int chunkId;
/**
* The file descriptor.
*/
private final FileDescriptor fileDescriptor;
/**
* The file size.
*/
private final int fileSize;
/**
* Creates the 'on-demand' response.
*
* @param fileDescriptor The file descriptor.
* @param fileSize The file size.
* @param chunkId The chunk id.
* @param chunkData The chunk data.
*/
public OnDemandResponse(FileDescriptor fileDescriptor, int fileSize, int chunkId, ByteBuf chunkData) {
this.fileDescriptor = fileDescriptor;
this.fileSize = fileSize;
this.chunkId = chunkId;
this.chunkData = chunkData;
}
/**
* Gets the chunk data.
*
* @return The chunk data.
*/
public ByteBuf getChunkData() {
return chunkData;
}
/**
* Gets the chunk id.
*
* @return The chunk id.
*/
public int getChunkId() {
return chunkId;
}
/**
* Gets the file descriptor.
*
* @return The file descriptor.
*/
public FileDescriptor getFileDescriptor() {
return fileDescriptor;
}
/**
* Gets the file size.
*
* @return The file size.
*/
public int getFileSize() {
return fileSize;
}
}
@@ -0,0 +1,31 @@
package org.apollo.net.codec.update;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
import org.apollo.cache.FileDescriptor;
import org.apollo.net.codec.update.OnDemandRequest.Priority;
/**
* A {@link ByteToMessageDecoder} for the 'on-demand' protocol.
*
* @author Graham
*/
public final class UpdateDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) {
if (buffer.readableBytes() >= 4) {
int type = buffer.readUnsignedByte() + 1;
int file = buffer.readUnsignedShort();
Priority priority = Priority.valueOf(buffer.readUnsignedByte());
FileDescriptor desc = new FileDescriptor(type, file);
out.add(new OnDemandRequest(desc, priority));
}
}
}
@@ -0,0 +1,35 @@
package org.apollo.net.codec.update;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.util.List;
import org.apollo.cache.FileDescriptor;
/**
* A {@link MessageToMessageEncoder} for the 'on-demand' protocol.
*
* @author Graham
*/
public final class UpdateEncoder extends MessageToMessageEncoder<OnDemandResponse> {
@Override
protected void encode(ChannelHandlerContext ctx, OnDemandResponse response, List<Object> out) {
FileDescriptor descriptor = response.getFileDescriptor();
int fileSize = response.getFileSize();
int chunkId = response.getChunkId();
ByteBuf chunkData = response.getChunkData();
ByteBuf buffer = ctx.alloc().buffer(2 * Byte.BYTES + 2 * Short.BYTES + chunkData.readableBytes());
buffer.writeByte(descriptor.getType() - 1);
buffer.writeShort(descriptor.getFile());
buffer.writeShort(fileSize);
buffer.writeByte(chunkId);
buffer.writeBytes(chunkData);
out.add(buffer);
}
}
@@ -0,0 +1,4 @@
/**
* Contains codecs for the ondemand (update) protocol.
*/
package org.apollo.net.codec.update;
@@ -0,0 +1,32 @@
package org.apollo.net.message;
/**
* A message sent by the client that can be intercepted.
*
* @author Graham
* @author Major
*/
public abstract class Message {
/**
* Indicates whether or not the Message chain has been terminated.
*/
private boolean terminated;
/**
* Terminates the Message chain.
*/
public final void terminate() {
terminated = true;
}
/**
* Returns whether or not the Message chain has been terminated.
*
* @return {@code true} if the Message chain has been terminated, otherwise {@code false}.
*/
public final boolean terminated() {
return terminated;
}
}
@@ -0,0 +1,4 @@
/**
* Contains message-related classes.
*/
package org.apollo.net.message;
@@ -0,0 +1,84 @@
package org.apollo.net.meta;
import com.google.common.base.Preconditions;
/**
* A class containing meta data for a single type of packet.
*
* @author Graham
*/
public final class PacketMetaData {
/**
* Creates packet meta data for a fixed-length packet.
*
* @param length The length of the packet.
* @return The packet meta data.
* @throws IllegalArgumentException If {@code length} is less than 0.
*/
public static PacketMetaData createFixed(int length) {
Preconditions.checkArgument(length >= 0, "Packet length cannot be less than 0.");
return new PacketMetaData(PacketType.FIXED, length);
}
/**
* Creates a packet meta data object for a variable byte length packet.
*
* @return The packet meta data object.
*/
public static PacketMetaData createVariableByte() {
return new PacketMetaData(PacketType.VARIABLE_BYTE, 0);
}
/**
* Creates a packet meta data object for a variable short length packet.
*
* @return The packet meta data object.
*/
public static PacketMetaData createVariableShort() {
return new PacketMetaData(PacketType.VARIABLE_SHORT, 0);
}
/**
* The length of this packet.
*/
private final int length;
/**
* The type of packet.
*/
private final PacketType type;
/**
* Creates the packet meta data object. This should not be called directly. Use the {@link #createFixed},
* {@link #createVariableByte}, and {@link #createVariableShort} methods instead!
*
* @param type The type of packet.
* @param length The length of the packet.
*/
private PacketMetaData(PacketType type, int length) {
this.type = type;
this.length = length;
}
/**
* Gets the length of this packet.
*
* @return The length of this packet.
* @throws IllegalStateException If the packet is not a fixed-size packet.
*/
public int getLength() {
Preconditions.checkState(type == PacketType.FIXED, "Can only get the length of a fixed length packet.");
return length;
}
/**
* Gets the type of packet.
*
* @return The type of packet.
*/
public PacketType getType() {
return type;
}
}
@@ -0,0 +1,64 @@
package org.apollo.net.meta;
import com.google.common.base.Preconditions;
/**
* A class containing a group of {@link PacketMetaData} objects.
*
* @author Graham
*/
public final class PacketMetaDataGroup {
/**
* Creates a packet meta data group from the packet length array.
*
* @param lengths The packet lengths.
* @return The packet meta data group.
* @throws IllegalArgumentException If the array length is not 256, or if there is an element in the array with a
* value below -3.
*/
public static PacketMetaDataGroup createFromArray(int[] lengths) {
Preconditions.checkArgument(lengths.length == 256, "Array length must be 256.");
PacketMetaDataGroup group = new PacketMetaDataGroup();
for (int index = 0; index < lengths.length; index++) {
int length = lengths[index];
Preconditions.checkArgument(length >= -2, "No packet length can have a value less than -3.");
PacketMetaData metaData = null;
if (length == -2) {
metaData = PacketMetaData.createVariableShort();
} else if (length == -1) {
metaData = PacketMetaData.createVariableByte();
} else {
metaData = PacketMetaData.createFixed(length);
}
group.packets[index] = metaData;
}
return group;
}
/**
* The array of packet meta data objects.
*/
private final PacketMetaData[] packets = new PacketMetaData[256];
/**
* This constructor should not be called directly. Use the {@link #createFromArray} method instead.
*/
private PacketMetaDataGroup() {
}
/**
* Gets the meta data for the specified packet.
*
* @param opcode The opcode of the packet.
* @return The {@link PacketMetaData}, or {@code null} if the packet does not exist.
* @throws IllegalArgumentException If the opcode is less than 0, or greater than 255.
*/
public PacketMetaData getMetaData(int opcode) {
Preconditions.checkElementIndex(opcode, packets.length, "Opcode out of bounds.");
return packets[opcode];
}
}
@@ -0,0 +1,30 @@
package org.apollo.net.meta;
/**
* An enumeration which contains the different types of packets.
*
* @author Graham
*/
public enum PacketType {
/**
* A packet where the length is known by both the client and server already.
*/
FIXED,
/**
* A packet with no header.
*/
RAW,
/**
* A packet where the length is sent to its destination with it as a byte.
*/
VARIABLE_BYTE,
/**
* A packet where the length is sent to its destination with it as a short.
*/
VARIABLE_SHORT;
}
@@ -0,0 +1,4 @@
/**
* Contains classes which contain meta data about the protocol/various packets.
*/
package org.apollo.net.meta;
@@ -0,0 +1,5 @@
/**
* Contains classes related to networking. Many of these extend Netty's set of classes - such as the pipeline factory,
* handler and codecs.
*/
package org.apollo.net;
@@ -0,0 +1,23 @@
package org.apollo.net.release;
import org.apollo.net.codec.game.GamePacket;
import org.apollo.net.message.Message;
/**
* A {@link MessageDecoder} decodes a {@link GamePacket} into a {@link Message} object which can be processed by the
* server.
*
* @author Graham
* @param <M> The type of message.
*/
public abstract class MessageDecoder<M extends Message> {
/**
* Decodes the specified packet into a message.
*
* @param packet The packet.
* @return The message.
*/
public abstract M decode(GamePacket packet);
}
@@ -0,0 +1,22 @@
package org.apollo.net.release;
import org.apollo.net.codec.game.GamePacket;
import org.apollo.net.message.Message;
/**
* A {@link MessageEncoder} encodes {@link Message} objects into {@link GamePacket}s which can be sent over the network.
*
* @author Graham
* @param <M> The type of message.
*/
public abstract class MessageEncoder<M extends Message> {
/**
* Encodes the specified message into a packet.
*
* @param message The message.
* @return The packet.
*/
public abstract GamePacket encode(M message);
}
@@ -0,0 +1,114 @@
package org.apollo.net.release;
import java.util.HashMap;
import java.util.Map;
import org.apollo.net.message.Message;
import org.apollo.net.meta.PacketMetaData;
import org.apollo.net.meta.PacketMetaDataGroup;
import com.google.common.base.Preconditions;
/**
* A {@link Release} is a distinct client version, e.g. {@code 317}.
*
* @author Graham
*/
public abstract class Release {
/**
* The array of message decoders.
*/
private final MessageDecoder<?>[] decoders = new MessageDecoder<?>[256];
/**
* The map of message classes to message encoders.
*/
private final Map<Class<? extends Message>, MessageEncoder<?>> encoders = new HashMap<>();
/**
* The incoming packet meta data.
*/
private final PacketMetaDataGroup incomingPacketMetaData;
/**
* The release number, e.g. {@code 317}.
*/
private final int releaseNumber;
/**
* Creates the release.
*
* @param releaseNumber The release number.
* @param incomingPacketMetaData The incoming packet meta data.
*/
public Release(int releaseNumber, PacketMetaDataGroup incomingPacketMetaData) {
this.releaseNumber = releaseNumber;
this.incomingPacketMetaData = incomingPacketMetaData;
}
/**
* Gets meta data for the specified incoming packet.
*
* @param opcode The opcode of the incoming packet.
* @return The {@link PacketMetaData} object.
*/
public final PacketMetaData getIncomingPacketMetaData(int opcode) {
return incomingPacketMetaData.getMetaData(opcode);
}
/**
* Gets the {@link MessageDecoder} for the specified opcode.
*
* @param opcode The opcode.
* @return The message decoder.
* @throws IndexOutOfBoundsException If the opcode is less than 0, or greater than 255.
*/
public final MessageDecoder<?> getMessageDecoder(int opcode) {
Preconditions.checkElementIndex(opcode, decoders.length, "Opcode out of bounds.");
return decoders[opcode];
}
/**
* Gets the {@link MessageEncoder} for the specified message type.
*
* @param type The type of message.
* @return The message encoder.
*/
@SuppressWarnings("unchecked")
public <M extends Message> MessageEncoder<M> getMessageEncoder(Class<M> type) {
return (MessageEncoder<M>) encoders.get(type);
}
/**
* Gets the release number.
*
* @return The release number.
*/
public final int getReleaseNumber() {
return releaseNumber;
}
/**
* Registers a {@link MessageEncoder} for the specified message type.
*
* @param type The message type.
* @param encoder The message encoder.
*/
public final <M extends Message> void register(Class<M> type, MessageEncoder<M> encoder) {
encoders.put(type, encoder);
}
/**
* Registers a {@link MessageDecoder} for the specified opcode.
*
* @param opcode The opcode, between 0 and 255 inclusive.
* @param decoder The message decoder.
* @throws IndexOutOfBoundsException If the opcode is less than 0, or greater than 255.
*/
public final <M extends Message> void register(int opcode, MessageDecoder<M> decoder) {
Preconditions.checkElementIndex(opcode, decoders.length, "Opcode out of bounds.");
decoders[opcode] = decoder;
}
}
@@ -0,0 +1,5 @@
/**
* Contains abstract classes which should be extended by a particular release, allowing for portability between various
* protocol and client releases.
*/
package org.apollo.net.release;
@@ -0,0 +1,52 @@
package org.apollo.net.update;
import io.netty.channel.Channel;
/**
* A specialised request which contains a channel as well as the request object itself.
*
* @author Graham
* @param <T> The type of request.
*/
public class ChannelRequest<T> {
/**
* The channel.
*/
private final Channel channel;
/**
* The request.
*/
protected final T request;
/**
* Creates a new channel request.
*
* @param channel The channel.
* @param request The request.
*/
public ChannelRequest(Channel channel, T request) {
this.channel = channel;
this.request = request;
}
/**
* Gets the channel.
*
* @return The channel.
*/
public Channel getChannel() {
return channel;
}
/**
* Gets the request.
*
* @return The request.
*/
public T getRequest() {
return request;
}
}
@@ -0,0 +1,29 @@
package org.apollo.net.update;
import io.netty.channel.Channel;
/**
* A {@link ChannelRequest} with a {@link Comparable} request type.
*
* @author Major
*
* @param <T> The type of request.
*/
public final class ComparableChannelRequest<T extends Comparable<T>> extends ChannelRequest<T> implements Comparable<ComparableChannelRequest<T>> {
/**
* Creates the ComparableChannelRequest.
*
* @param channel The {@link Channel} making the request.
* @param request The request.
*/
public ComparableChannelRequest(Channel channel, T request) {
super(channel, request);
}
@Override
public int compareTo(ComparableChannelRequest<T> o) {
return request.compareTo(o.request);
}
}
@@ -0,0 +1,148 @@
package org.apollo.net.update;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.Optional;
import org.apollo.cache.IndexedFileSystem;
import org.apollo.net.update.resource.CombinedResourceProvider;
import org.apollo.net.update.resource.HypertextResourceProvider;
import org.apollo.net.update.resource.ResourceProvider;
import org.apollo.net.update.resource.VirtualResourceProvider;
import com.google.common.base.Charsets;
/**
* A worker which services HTTP requests.
*
* @author Graham
*/
public final class HttpRequestWorker extends RequestWorker<HttpRequest, ResourceProvider> {
/**
* The default character set.
*/
private static final Charset CHARACTER_SET = Charsets.ISO_8859_1;
/**
* The value of the server header.
*/
private static final String SERVER_IDENTIFIER = "JAGeX/3.1";
/**
* The directory with web files.
*/
private static final Path WWW_DIRECTORY = Paths.get("data/www");
/**
* Creates the HTTP request worker.
*
* @param dispatcher The dispatcher.
* @param fs The file system.
*/
public HttpRequestWorker(UpdateDispatcher dispatcher, IndexedFileSystem fs) {
super(dispatcher, new CombinedResourceProvider(new VirtualResourceProvider(fs), new HypertextResourceProvider(WWW_DIRECTORY)));
}
/**
* Creates an error page.
*
* @param status The HTTP status.
* @param description The error description.
* @return The error page as a buffer.
*/
private static ByteBuf createErrorPage(HttpResponseStatus status, String description) {
String title = status.code() + " " + status.reasonPhrase();
StringBuilder builder = new StringBuilder("<!DOCTYPE html><html><head><title>");
builder.append(title);
builder.append("</title></head><body><h1>");
builder.append(title);
builder.append("</h1><p>");
builder.append(description);
builder.append("</p><hr /><address>");
builder.append(SERVER_IDENTIFIER);
builder.append(" Server</address></body></html>");
return Unpooled.copiedBuffer(builder.toString(), Charset.defaultCharset());
}
/**
* Gets the MIME type of a file by its name.
*
* @param name The file name.
* @return The MIME type.
*/
private static String getMimeType(String name) {
if (name.endsWith("/")) {
name = name.concat("index.html");
}
if (name.endsWith(".htm") || name.endsWith(".html")) {
return "text/html";
} else if (name.endsWith(".css")) {
return "text/css";
} else if (name.endsWith(".js")) {
return "text/javascript";
} else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "image/jpeg";
} else if (name.endsWith(".gif")) {
return "image/gif";
} else if (name.endsWith(".png")) {
return "image/png";
} else if (name.endsWith(".txt")) {
return "text/plain";
}
return "application/octect-stream";
}
@Override
protected ChannelRequest<HttpRequest> nextRequest(UpdateDispatcher dispatcher) throws InterruptedException {
return dispatcher.nextHttpRequest();
}
@Override
protected void service(ResourceProvider provider, Channel channel, HttpRequest request) throws IOException {
String path = request.getUri();
Optional<ByteBuffer> buf = provider.get(path);
HttpResponseStatus status = HttpResponseStatus.OK;
String mime = getMimeType(request.getUri());
if (!buf.isPresent()) {
status = HttpResponseStatus.NOT_FOUND;
mime = "text/html";
}
ByteBuf wrapped = buf.isPresent() ? Unpooled.wrappedBuffer(buf.get()) : createErrorPage(status, "The page you requested could not be found.");
HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), status);
response.headers().set("Date", new Date());
response.headers().set("Server", SERVER_IDENTIFIER);
response.headers().set("Content-type", mime + ", charset=" + CHARACTER_SET.name());
response.headers().set("Cache-control", "no-cache");
response.headers().set("Pragma", "no-cache");
response.headers().set("Expires", new Date(0));
response.headers().set("Connection", "close");
response.headers().set("Content-length", wrapped.readableBytes());
channel.write(response);
channel.writeAndFlush(wrapped).addListener(ChannelFutureListener.CLOSE);
}
}
@@ -0,0 +1,52 @@
package org.apollo.net.update;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Optional;
import org.apollo.cache.IndexedFileSystem;
import org.apollo.net.codec.jaggrab.JagGrabRequest;
import org.apollo.net.codec.jaggrab.JagGrabResponse;
import org.apollo.net.update.resource.ResourceProvider;
import org.apollo.net.update.resource.VirtualResourceProvider;
/**
* A worker which services JAGGRAB requests.
*
* @author Graham
*/
public final class JagGrabRequestWorker extends RequestWorker<JagGrabRequest, ResourceProvider> {
/**
* Creates the JAGGRAB request worker.
*
* @param dispatcher The dispatcher.
* @param fs The file system.
*/
public JagGrabRequestWorker(UpdateDispatcher dispatcher, IndexedFileSystem fs) {
super(dispatcher, new VirtualResourceProvider(fs));
}
@Override
protected ChannelRequest<JagGrabRequest> nextRequest(UpdateDispatcher dispatcher) throws InterruptedException {
return dispatcher.nextJagGrabRequest();
}
@Override
protected void service(ResourceProvider provider, Channel channel, JagGrabRequest request) throws IOException {
Optional<ByteBuffer> buffer = provider.get(request.getFilePath());
if (buffer.isPresent()) {
ByteBuf wrapped = Unpooled.wrappedBuffer(buffer.get());
channel.writeAndFlush(new JagGrabResponse(wrapped)).addListener(ChannelFutureListener.CLOSE);
} else {
channel.close();
}
}
}
@@ -0,0 +1,54 @@
package org.apollo.net.update;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import java.io.IOException;
import org.apollo.cache.FileDescriptor;
import org.apollo.cache.IndexedFileSystem;
import org.apollo.net.codec.update.OnDemandRequest;
import org.apollo.net.codec.update.OnDemandResponse;
/**
* A worker which services 'on-demand' requests.
*
* @author Graham
*/
public final class OnDemandRequestWorker extends RequestWorker<OnDemandRequest, IndexedFileSystem> {
/**
* The maximum length of a chunk, in {@code byte}s.
*/
private static final int CHUNK_LENGTH = 500;
/**
* Creates the 'on-demand' request worker.
*
* @param dispatcher The dispatcher.
* @param fs The file system.
*/
public OnDemandRequestWorker(UpdateDispatcher dispatcher, IndexedFileSystem fs) {
super(dispatcher, fs);
}
@Override
protected ChannelRequest<OnDemandRequest> nextRequest(UpdateDispatcher dispatcher) throws InterruptedException {
return dispatcher.nextOnDemandRequest();
}
@Override
protected void service(IndexedFileSystem fs, Channel channel, OnDemandRequest request) throws IOException {
FileDescriptor descriptor = request.getFileDescriptor();
ByteBuf buffer = Unpooled.wrappedBuffer(fs.getFile(descriptor));
int length = buffer.readableBytes();
for (int chunk = 0; buffer.readableBytes() > 0; chunk++) {
int chunkSize = Math.min(buffer.readableBytes(), CHUNK_LENGTH);
OnDemandResponse response = new OnDemandResponse(descriptor, length, chunk, buffer.readBytes(chunkSize));
channel.writeAndFlush(response);
}
}
}
@@ -0,0 +1,97 @@
package org.apollo.net.update;
import io.netty.channel.Channel;
import java.io.IOException;
/**
* The base class for request workers.
*
* @author Graham
* @param <T> The type of request.
* @param <P> The type of provider.
*/
public abstract class RequestWorker<T, P> implements Runnable {
/**
* The update dispatcher.
*/
private final UpdateDispatcher dispatcher;
/**
* The resource provider.
*/
private final P provider;
/**
* A flag indicating if the worker should be running.
*/
private boolean running = true;
/**
* Creates the request worker with the specified file system.
*
* @param dispatcher The update dispatcher.
* @param provider The resource provider.
*/
public RequestWorker(UpdateDispatcher dispatcher, P provider) {
this.provider = provider;
this.dispatcher = dispatcher;
}
/**
* Gets the next request.
*
* @param dispatcher The dispatcher.
* @return The next request.
* @throws InterruptedException If the thread is interrupted.
*/
protected abstract ChannelRequest<T> nextRequest(UpdateDispatcher dispatcher) throws InterruptedException;
@Override
public final void run() {
while (true) {
synchronized (this) {
if (!running) {
break;
}
}
ChannelRequest<T> request;
try {
request = nextRequest(dispatcher);
} catch (InterruptedException e) {
continue;
}
Channel channel = request.getChannel();
try {
service(provider, channel, request.getRequest());
} catch (IOException e) {
e.printStackTrace();
channel.close();
}
}
}
/**
* Services a request.
*
* @param provider The resource provider.
* @param channel The channel.
* @param request The request to service.
* @throws IOException If an I/O error occurs.
*/
protected abstract void service(P provider, Channel channel, T request) throws IOException;
/**
* Stops this worker. The worker's thread may need to be interrupted.
*/
public final void stop() {
synchronized (this) {
running = false;
}
}
}
@@ -0,0 +1,17 @@
package org.apollo.net.update;
/**
* Holds update-related constants.
*
* @author Graham
*/
public final class UpdateConstants {
/**
* Default private constructor to prevent instantiation by other classes.
*/
private UpdateConstants() {
}
}
@@ -0,0 +1,109 @@
package org.apollo.net.update;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.HttpRequest;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import org.apollo.net.codec.jaggrab.JagGrabRequest;
import org.apollo.net.codec.update.OnDemandRequest;
/**
* Dispatches update requests to worker threads.
*
* @author Graham
*/
public final class UpdateDispatcher {
/**
* The maximum size of a queue before requests are rejected.
*/
private static final int MAXIMUM_QUEUE_SIZE = 1024;
/**
* A queue for pending 'on-demand' requests.
*/
private final BlockingQueue<ComparableChannelRequest<OnDemandRequest>> demand = new PriorityBlockingQueue<>();
/**
* A queue for pending HTTP requests.
*/
private final BlockingQueue<ChannelRequest<HttpRequest>> http = new LinkedBlockingQueue<>();
/**
* A queue for pending JAGGRAB requests.
*/
private final BlockingQueue<ChannelRequest<JagGrabRequest>> jaggrab = new LinkedBlockingQueue<>();
/**
* Dispatches a HTTP request.
*
* @param channel The channel.
* @param request The request.
*/
public void dispatch(Channel channel, HttpRequest request) {
if (http.size() >= MAXIMUM_QUEUE_SIZE) {
channel.close();
}
http.add(new ChannelRequest<>(channel, request));
}
/**
* Dispatches a JAGGRAB request.
*
* @param channel The channel.
* @param request The request.
*/
public void dispatch(Channel channel, JagGrabRequest request) {
if (jaggrab.size() >= MAXIMUM_QUEUE_SIZE) {
channel.close();
}
jaggrab.add(new ChannelRequest<>(channel, request));
}
/**
* Dispatches an 'on-demand' request.
*
* @param channel The channel.
* @param request The request.
*/
public void dispatch(Channel channel, OnDemandRequest request) {
if (demand.size() >= MAXIMUM_QUEUE_SIZE) {
channel.close();
}
demand.add(new ComparableChannelRequest<>(channel, request));
}
/**
* Gets the next HTTP request from the queue, blocking if none are available.
*
* @return The HTTP request.
* @throws InterruptedException If the thread is interrupted.
*/
ChannelRequest<HttpRequest> nextHttpRequest() throws InterruptedException {
return http.take();
}
/**
* Gets the next JAGGRAB request from the queue, blocking if none are available.
*
* @return The JAGGRAB request.
* @throws InterruptedException If the thread is interrupted.
*/
ChannelRequest<JagGrabRequest> nextJagGrabRequest() throws InterruptedException {
return jaggrab.take();
}
/**
* Gets the next 'on-demand' request from the queue, blocking if none are available.
*
* @return The 'on-demand' request.
* @throws InterruptedException If the thread is interrupted.
*/
ChannelRequest<OnDemandRequest> nextOnDemandRequest() throws InterruptedException {
return demand.take();
}
}
@@ -0,0 +1,4 @@
/**
* Contains classes related to the update server.
*/
package org.apollo.net.update;
@@ -0,0 +1,43 @@
package org.apollo.net.update.resource;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Optional;
/**
* A resource provider composed of multiple resource providers.
*
* @author Graham
*/
public final class CombinedResourceProvider implements ResourceProvider {
/**
* An array of resource providers.
*/
private final ResourceProvider[] providers;
/**
* Creates the combined resource providers.
*
* @param providers The providers this provider delegates to.
*/
public CombinedResourceProvider(ResourceProvider... providers) {
this.providers = providers;
}
@Override
public boolean accept(String path) throws IOException {
return true;
}
@Override
public Optional<ByteBuffer> get(String path) throws IOException {
for (ResourceProvider provider : providers) {
if (provider.accept(path)) {
return provider.get(path);
}
}
return Optional.empty();
}
}
@@ -0,0 +1,67 @@
package org.apollo.net.update.resource;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
/**
* A {@link ResourceProvider} which provides additional hypertext resources.
*
* @author Graham
*/
public final class HypertextResourceProvider implements ResourceProvider {
/**
* The base {@link Path} from which documents are served.
*/
private final Path base;
/**
* Creates a new hypertext resource provider with the specified base directory.
*
* @param base The base directory.
*/
public HypertextResourceProvider(Path base) {
this.base = base;
}
@Override
public boolean accept(String path) throws IOException {
Path file = base.resolve(path);
URI target = file.toUri().normalize();
if (!target.toASCIIString().startsWith(base.toUri().normalize().toASCIIString())) {
return false;
}
if (Files.isDirectory(file)) {
file = file.resolve("index.html");
}
return Files.exists(file);
}
@Override
public Optional<ByteBuffer> get(String path) throws IOException {
Path root = base.resolve(path);
if (Files.isDirectory(root)) {
root = root.resolve("index.html");
}
if (!Files.exists(root)) {
return Optional.empty();
}
try (FileChannel channel = FileChannel.open(root)) {
ByteBuffer buf = channel.map(MapMode.READ_ONLY, 0, Files.size(root));
return Optional.of(buf);
}
}
}
@@ -0,0 +1,33 @@
package org.apollo.net.update.resource;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Optional;
/**
* A class which provides resources.
*
* @author Graham
*/
public interface ResourceProvider {
/**
* Checks that this provider can fulfil a request to the specified resource.
*
* @param path The path to the resource, e.g. {@code /crc}.
* @return {@code true} if the provider can fulfil a request to the resource, {@code false} otherwise.
* @throws IOException If an I/O error occurs.
*/
public boolean accept(String path) throws IOException;
/**
* Gets the resource data, as a {@link ByteBuffer}, wrapped in an {@link Optional}.
*
* @param path The path to the resource.
* @return A {@code ByteBuffer} representation of a resource if it exists otherwise {@link Optional#empty()} is
* returned.
* @throws IOException If some I/O exception occurs.
*/
public Optional<ByteBuffer> get(String path) throws IOException;
}
@@ -0,0 +1,71 @@
package org.apollo.net.update.resource;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apollo.cache.IndexedFileSystem;
/**
* A {@link ResourceProvider} which maps virtual resources (such as {@code /media}) to files in an
* {@link IndexedFileSystem}.
*
* @author Graham
*/
public final class VirtualResourceProvider implements ResourceProvider {
/**
* A {@link List} of valid prefixes.
*/
private static final List<String> VALID_PREFIXES = Arrays.asList("/crc", "/title", "/config", "/interface", "/media", "/versionlist", "/textures", "/wordenc", "/sounds");
/**
* The file system.
*/
private final IndexedFileSystem fs;
/**
* Creates a new virtual resource provider with the specified file system.
*
* @param fs The file system.
*/
public VirtualResourceProvider(IndexedFileSystem fs) {
this.fs = fs;
}
@Override
public boolean accept(String path) throws IOException {
Objects.requireNonNull(path);
return VALID_PREFIXES.stream().anyMatch(path::startsWith);
}
@Override
public Optional<ByteBuffer> get(String path) throws IOException {
if (path.startsWith("/crc")) {
return Optional.of(fs.getCrcTable());
} else if (path.startsWith("/title")) {
return Optional.of(fs.getFile(0, 1));
} else if (path.startsWith("/config")) {
return Optional.of(fs.getFile(0, 2));
} else if (path.startsWith("/interface")) {
return Optional.of(fs.getFile(0, 3));
} else if (path.startsWith("/media")) {
return Optional.of(fs.getFile(0, 4));
} else if (path.startsWith("/versionlist")) {
return Optional.of(fs.getFile(0, 5));
} else if (path.startsWith("/textures")) {
return Optional.of(fs.getFile(0, 6));
} else if (path.startsWith("/wordenc")) {
return Optional.of(fs.getFile(0, 7));
} else if (path.startsWith("/sounds")) {
return Optional.of(fs.getFile(0, 8));
}
return Optional.empty();
}
}
@@ -0,0 +1,4 @@
/**
* Contains resource providers for the update server.
*/
package org.apollo.net.update.resource;
@@ -0,0 +1,76 @@
package org.apollo.net.codec.game;
import static org.junit.Assert.assertEquals;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.util.ArrayList;
import java.util.List;
import org.apollo.net.meta.PacketType;
import org.apollo.util.security.IsaacRandom;
import org.junit.Test;
/**
* Contains tests for {@link GamePacketEncoder}.
*
* @author Graham
*/
public class TestGamePacketEncoder {
/**
* Tests the {@link GamePacketEncoder#encode} method.
*
* @throws Exception If an error occurs.
*/
@Test
public void testEncode() throws Exception {
// generates 243, 141, 34, -223, 121...
IsaacRandom random = new IsaacRandom(new int[] { 0, 0, 0, 0 });
GamePacketEncoder encoder = new GamePacketEncoder(random);
ByteBuf payload = Unpooled.wrappedBuffer("Hello".getBytes());
GamePacket packet = new GamePacket(10, PacketType.FIXED, payload.copy());
List<Object> out = new ArrayList<>();
encoder.encode(null, packet, out);
ByteBuf buf = (ByteBuf) out.get(0);
assertEquals(6, buf.readableBytes());
assertEquals(253, buf.readUnsignedByte());
assertEquals('H', buf.readUnsignedByte());
assertEquals('e', buf.readUnsignedByte());
assertEquals('l', buf.readUnsignedByte());
assertEquals('l', buf.readUnsignedByte());
assertEquals('o', buf.readUnsignedByte());
packet = new GamePacket(9, PacketType.VARIABLE_BYTE, payload.copy());
out.clear();
encoder.encode(null, null, out);
buf = (ByteBuf) out.get(0);
assertEquals(7, buf.readableBytes());
assertEquals(150, buf.readUnsignedByte());
assertEquals(5, buf.readUnsignedByte());
assertEquals('H', buf.readUnsignedByte());
assertEquals('e', buf.readUnsignedByte());
assertEquals('l', buf.readUnsignedByte());
assertEquals('l', buf.readUnsignedByte());
assertEquals('o', buf.readUnsignedByte());
packet = new GamePacket(0, PacketType.VARIABLE_SHORT, payload.copy());
out.clear();
encoder.encode(null, packet, out);
buf = (ByteBuf) out.get(0);
assertEquals(8, buf.readableBytes());
assertEquals(34, buf.readUnsignedByte());
assertEquals(5, buf.readUnsignedShort());
assertEquals('H', buf.readUnsignedByte());
assertEquals('e', buf.readUnsignedByte());
assertEquals('l', buf.readUnsignedByte());
assertEquals('l', buf.readUnsignedByte());
assertEquals('o', buf.readUnsignedByte());
}
}