mirror of
https://github.com/2006-Scape/apollo.git
synced 2026-07-06 00:38:18 +00:00
Modularise! Also add some unit tests.
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user