Skip to content

Instantly share code, notes, and snippets.

@Jire
Created July 4, 2011 00:03
Show Gist options
  • Save Jire/1062730 to your computer and use it in GitHub Desktop.
Save Jire/1062730 to your computer and use it in GitHub Desktop.
Performs decoding measures which decodes a new connection into a login request which is then processed by the server to enable game access for the user.
/*
* Nital is an effort to provide a well documented, powerful, scalable, and robust
* RuneScape server framework delivered open-source to all users.
*
* Copyright (C) 2011 Nital Software
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package us.nital.net.codec;
import java.security.SecureRandom;
import java.util.logging.Logger;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.replay.ReplayingDecoder;
import us.nital.net.Session;
import us.nital.net.io.OutBuffer;
import us.nital.util.BufferUtils;
import us.nital.util.NameUtils;
import us.nital.world.Result;
import us.nital.world.ReturnCodes;
import us.nital.world.World;
/**
* Performs decoding measures which decodes a new connection into
* a login request which is then processed by the server to enable
* game access for the user.
*
* <h2>Things to Note</h2>
* <ul>
* <li>All operation codes are stored in one class in the client.
* This means that the codes are notifiers of only <b>one</b> action
* and will not be reused.</li>
* </ul>
*
* @author Thomas Nappo
* @see {@link State}
*/
public class FrontDecoder extends ReplayingDecoder<FrontDecoder.State> {
/**
* The state of the decoding process.
* @author Thomas Nappo
*/
protected static enum State {
/**
* At this state the client sends a decode request
* to the server. Here the server identifies what state
* we it needs to perform by reading an unsigned <code>byte</code>.</p>
*
* According to debugging statistics the byte attributes these values:
* <ui>
* <li>14: The client requested to login (state: {@link #SERVER_CHOICE})
* <li>15: The client requested an update (state: {@link #UPDATE}
* </ui>
*/
REQUEST,
/**
* At this state the client is sent cache indices which,
* when received by the client, update the user's local
* cache to match the server's.
*/
UPDATE,
/**
* At this state the server decides the best login server
* choice for the user.
*/
SERVER_CHOICE,
/**
* At this state the server has finished decoding and now
* provides the client with information to login after
* authenticating the username and password sent by the client.
*
* <p>The server at this time swaps the pipeline's decoder to
* {@link #Decoder}.</p>
*/
GAME;
}
/**
* Constructs a new front decoder which passes <code>false</code>
* to the parent decoder, making the decoding stage not unfold.
* It also checkpoints the state {@link State#REQUEST}.
*/
public FrontDecoder() {
super(false);
checkpoint(State.REQUEST);
}
/**
* The client receives this response to get ready for an update.
*/
private static final byte[] INITIAL_RESPONSE = {
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0
};
/**
* This singleton instance is used to generate random numbers
* during the decoding process.
*/
private static final SecureRandom RANDOM = new SecureRandom();
/**
* Used to log component messages out towards the console during
* the decoding stages.
*/
private static final Logger logger = Logger.getLogger(FrontDecoder.class.getName());
// TODO: At a later time we might want to switch them to
// be departed from this class, but this is okay for a
// smaller decode (such as that in the 317 protocol).
/**
* This key is used for security, in order to make sure
* the client who has began logging in is also the same
* one during the authentication.
*/
private long serverKey;
@Override
protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state) {
switch (state) {
default:
case REQUEST:
/*
* The server requires at least one byte
* to be read for the request operation code.
*/
if (buffer.readableBytes() < 1) {
return false;
}
/*
* We read an unsigned byte which contains
* the operation code of the request.
*/
switch (buffer.readUnsignedByte()) {
case 14:
checkpoint(State.SERVER_CHOICE);
/*
* We return back true because we did read.
*/
return true;
}
/*
* If the value was not specified we return
* false because the reading was not successful.
*/
return false;
case SERVER_CHOICE:
/*
* The server requires at least one byte
* to be read for the name hash.
*/
if (buffer.readableBytes() < 1) {
return false;
}
/*
* We read an unsigned byte value. This value is used
* to find the best login server for a user.
*/
@SuppressWarnings("unused")
int nameHash = buffer.readUnsignedByte();
/*
* To the channel we write a response.
*/
channel.write(new OutBuffer()
/*
* These are the initial login response
* keys which we write to the buffer.
*/
.write(INITIAL_RESPONSE)
/*
* This is a notification key which might
* be there for some authentication, but as
* far as the client shows, this is used to
* separate the randomly generated server key
* from the initiial response buffers.
*/
.write(0)
/*
* We generate a random long using the SecureRandom
* constant and set it to the field serverKey
*/
.writeLong(serverKey = RANDOM.nextLong()));
/*
* We checkpoint the GAME state.
*/
checkpoint(State.GAME);
/*
* Return back true, because we did
* read from the buffer and the decoding
* state was completed successfully.
*/
return true;
case GAME:
/*
* The server requires at least two bytes
* of data to be read. The first is the login
* operation code and the second is the login
* packet's size.
*/
if (buffer.readableBytes() < 2) {
return false;
}
/*
* We read an operation code which signals what
* type of login request should be serviced.
*/
int loginOpCode = buffer.readUnsignedByte();
/*
* We switch the operation code to a case-system.
* Upon the right case the login will be serviced
* as so.
*/
switch (loginOpCode) {
/*
* Case 16 is a normal connection.
*/
case 16:
/*
* Case 18 is a reconnection. Reconnections take
* place when the game server drops the user and
* the client quickly reconnects the user.
*/
case 18:
/*
* Read the size of the login packet.
*/
int loginSize = buffer.readUnsignedByte();
/*
* Calculcate the encrypted buffer size.
*/
int loginSizeE = loginSize - (36 + 1 + 1 + 2);
/*
* A size, regardless of what that size measures
* cannot be less than 1.
*
* Considering we cannot instantize a new buffer
* with negative or no capacity, we'll end the
* connection.
*/
if (loginSizeE < 1) {
channel.close();
return false;
}
/*
* The client reported the buffer size to the server.
* If the buffer has less than the size it's impossible
* to read and in the future some sort of error will occur.
*/
if (buffer.readableBytes() < loginSize) {
return false;
}
/*
* This is yet another security check. We read an unsigned
* byte from the buffer which must be equivalent to 255.
*/
if (buffer.readUnsignedByte() != 255) {
channel.close();
return false;
}
/*
* This is the client build which is used to associate
* the version of the client. Because the decoding stage
* is setup for a specific protocol (in our case 317) we'll
* stop the decoding if it doesn't match our number.
*/
if (buffer.readUnsignedShort() != 317) {
channel.close();
return false;
}
/*
* This value indicates whether or not the client is in
* high memory version. If so, the client displays various
* detail objects like grass and flowers. In the 317 protocol
* thee server does not send sounds to low memory version clients.
*/
@SuppressWarnings("unused")
boolean highMemoryVersion = buffer.readUnsignedByte() == 0;
/*
* The buffer skips over these values. They are known as
* cache indices, as they separate the index form inside
* the game cache.
*/
for (int i = 0; i < 9; i++) buffer.readInt();
/*
* We subtract one from the size because it already included
* the size byte.
*/
loginSizeE--;
/*
* Here the client reports the expected encrypted buffer size
* to the server. The server also already calculated the size,
* so these must match.
*/
if (buffer.readUnsignedByte() != loginSizeE) {
channel.close();
return false;
}
/*
* We now check if the reported RSA code is 10.
*
* This could be variable if an RSA generator was
* enabled however most clients have it disabled.
*/
if (buffer.readUnsignedByte() != 10) {
channel.close();
return false;
}
/*
* The client reports it's own generated random key
* which is unique to the connection session.
*/
@SuppressWarnings("unused")
long clientKey = buffer.readLong();
/*
* The server now reads the randomly generated key
* which was sent to the client previously.
*
* If they do not match a connection interference
* might have taken place therefore we end service.
*/
if (buffer.readLong() != serverKey) {
channel.close();
return false;
}
/*
* This is the user identification key.
*
* Some clients send random UID keys. Only check
* the key's value if your client has it enabled.
*/
@SuppressWarnings("unused")
int uid = buffer.readInt();
/*
* This is the request's username. We read it from the buffer
* using some utilities to format the name as well.
*/
String username = NameUtils.formatName(BufferUtils.getRS2String(buffer));
/*
* This is the request's password.
*/
String password = BufferUtils.getRS2String(buffer);
/*
* We output the request towards the console.
*/
logger.info("Login request: (" + username + "," + password + ")");
/*
* We create a new game session.
*/
Session session = new Session(username, password, channel);
/*
* And register that new instance to the world, which returns
* back our needed result.
*/
Result result = World.getSingleton().register(session);
/*
* And grab the result's return code attachment to write out.
* This code tells the client what messages to display in a failed
* login attempt.
*/
buffer.writeByte((Integer) result.getAttachment("returnCode"));
/*
* If the result's return code attachment was not the success code
* we end the connection.
*/
if (((Integer) result.getAttachment("returnCode")) != ReturnCodes.SUCCESS) {
channel.close();
return false;
}
/*
* We now write out flags which were attached to the result which
* configure the client to perform certain actions while the user
* is playing.
*/
// TODO {result.getAttachment("flagged")}
/*
* With all said and done we return back true because of all
* the writing and reading we've done.
*/
return true;
/*
* Any other cases cannot be serviced.
*/
default:
return false;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment