Removing thread per client - Stops SYI Attacks fully

Results 1 to 1 of 1
  1. #1
    Novice Feathers is offline
    MemberRank
    May 2009 Join Date
    1Posts

    Removing thread per client - Stops SYI Attacks fully

    Assumed Knowledge: How to read, adding new classes, editing classes, common sense.

    Tested Server: Winterlove, so should work on all Winterlove-based servers.

    Files/Classes Modified: client, server, PlayerHandler

    Files/Classes Added: IOClient, IOHostList, IOThread

    Difficulty: Dunno, it's all explained and there are some servers available for down pre-patched (at the top)

    Procedure

    Let me remind you again to BACKUP YOUR FILES. This tutorial, if applied incorrectly COULD STOP THE ABILITY TO LOGIN. Backup! Backup!

    I'm also going to explain how things work as we go along, so you can (hopefully) get a better understanding of how the core processes work inside a server. This should help you understand how the login process works, and how things can be optimized.

    So first of all, lets look at the way a standard Winterlove server works.

    There is one thread started initially: this thread listens on the socket and deals with new clients. The main thread processes existing clients.

    When a client joins, the server will call the PlayerHandler and add a new client. The player handler starts a new thread for this client. The thread then manages the login process and after that it manages the client I/O.

    We are going to change this to work with just two threads.

    We will (again) have the thread to listen on the socket, and we also have another thread to deal with not logged in clients. The default thread again deals with logged in clients.

    When a client joins, the server will call the PlayerHandler and add a new client. This client is not the full client structure (saves memory) - it is the class named IOClient (don't ask lol). The thread that deals with not logged in clients will eventually process this IOClient and then, if a login is successful, the real client object will be created. The default thread deals with client logic and IO.

    STEP 1: Addding the IOClient, IOHostList and IOThread classes and modifying the PlayerHandler class

    First of all we are going to add the IOClient class.

    I will explain how it works first, then give you the whole thing to copy and paste.
    Code:
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.ObjectInputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    We are importing classes from the Java standard library, need I explain any more?



    Code:
    /**
     * Represents a client that has connected but has not yet logged in.
     * @author Graham
     *
     */
    public class IOClient {
    Defining the class, speaks for itself again.


    Code:
    	/**
    	 * When the client connected.
    	 */
    	private long connectedAt;
    
    	/**
    	 * The timeout value in seconds.
    	 */
    	private static final int TIMEOUT = 1;
    This variable is used to detect client timeouts. For instance, some socket flooders will connect then sit and do nothing. This will stop idle connections. The first variable is a long and stores the time of connection. The second is a constant that stores the timeout value. So, with this setup, once a client has been idle for 1 second it will be disconnected. You may need to increase the timeout if your server is going to be busy and/or has users with slow connections.


    Code:
    /**
    	 * The client's current state.
    	 * @author Graham
    	 *
    	 */
    	private static enum State {
    		LOGIN_START,
    		LOGIN_READ1,
    		LOGIN_READ2,
    	}
    
    	private State state = State.LOGIN_START;
    We need to remember which part of the login process we are in - since we are single threaded. For this I use a state. The client, by default, is in the LOGIN_START state. Then, once it has started login, it is switched to the LOGIN_READ1 state. Finally, when that is complete, it is set to the LOGIN_READ2 state and the final part of the login process takes place.

    Code:
    private Socket socket;
    	private String connectedFrom;
    	
    	private Stream outStream = new stream(new byte[client.bufferSize]);
    	private Stream inStream = new stream(new byte[client.bufferSize]);
    	private InputStream in;
    	private OutputStream out;
    	private Cryption inStreamDecryption;
    	private Cryption outStreamDecryption;
    	
    	private long serverSessionKey = 0, clientSessionKey = 0;
    	private int loginPacketSize, loginEncryptPacketSize;
    	
    	private String playerName = null, playerPass = null;
    	
    	public PlayerHandler handler;
    All of that is self explanatory: it is parts of the client class that our IOClient class also needs.
    Code:
    	public IOClient(Socket s, String connectedFrom) throws IOException {
    		this.socket = s;
    		this.connectedFrom = connectedFrom;
    		this.outStream.currentOffset = 0;
    		this.inStream.currentOffset = 0;
    		this.in = s.getInputStream();
    		this.out = s.getOutputStream();
    		this.serverSessionKey = ((long)(java.lang.Math.random() * 99999999D) << 32) + (long)(java.lang.Math.random() * 99999999D);
    		this.connectedAt = System.currentTimeMillis();
    		IOHostList.add(connectedFrom);
    	}
    This is the constructor: this code is called when a new IOClient is created. Again, it is all pretty standard and easy to understand. Also note the IOHostList: this will help prevent against SYIpkpker attacks by only allowing three connections per IP address.

    Code:
    public void destruct(boolean close) {
    		if(close && this.socket != null) {
    			IOHostList.remove(connectedFrom);
    			try {
    				this.socket.close();
    			} catch(Exception e) {}
    		}
    		this.socket = null;
    		this.outStream = null;
    		this.inStream = null;
    		this.in = null;
    		this.out = null;
    	}
    This method will close the various streams and the socket (if the flag is set). Self explanatory.


    Code:
    public boolean process() throws Exception, IOException {
    		long diff = System.currentTimeMillis() - connectedAt;
    		if(diff > (TIMEOUT*1000)) {
    			throw new Exception("Timeout.");
    		}
    The process method for IOClients is called every 30ms as opposed to every 500ms.

    The first few statements deal with the timeout. If the client did timeout, an exception is thrown.



    Code:
    if(state == State.LOGIN_START) {
    			if(fillinStream(2)) {
    				if(inStream.readUnsignedByte() != 14) {
    					throw new Exception("Expect login byte 14 from client.");
    				}
    				// this is part of the usename. Maybe it's used as a hash to select the appropriate
    				// login server
    				@SuppressWarnings("unused")
    				int namePart = inStream.readUnsignedByte();
    				for(int i = 0; i < 8; i++) out.write(0);		// is being ignored by the client
    				// login response - 0 means exchange session key to establish encryption
    				// Note that we could use 2 right away to skip the cryption part, but i think this
    				// won't work in one case when the cryptor class is not set and will throw a NullPointerException
    				out.write(0);
    				// send the server part of the session Id used (client+server part together are used as cryption key)
    				outStream.writeQWord(serverSessionKey);
    				directFlushoutStream();
    				state = State.LOGIN_READ1;
    			}
    This handles the first stage of the login process. Note the if(fillinStream(2)), this will be explained later.


    Code:
    } else if(state == State.LOGIN_READ1) {
    			if(fillinStream(2)) {
    				int loginType = inStream.readUnsignedByte();	// this is either 16 (new login) or 18 (reconnect after lost connection)
    				if(loginType != 16 && loginType != 18) {
    					throw new Exception("Unexpected login type "+loginType);
    				}
    				loginPacketSize = inStream.readUnsignedByte();
    				loginEncryptPacketSize = loginPacketSize-(36+1+1+2);	// the size of the RSA encrypted part (containing password)
    				misc.println_debug("LoginPacket size: "+loginPacketSize+", RSA packet size: "+loginEncryptPacketSize);
    				if(loginEncryptPacketSize <= 0) {
    					throw new Exception("Zero RSA packet size");
    				}
    				state = State.LOGIN_READ2;
    			}
    This handles the second stage of the login process.


    Code:
    } else if(state == State.LOGIN_READ2) {
    			if(fillinStream(loginPacketSize)) {
    				if(inStream.readUnsignedByte() != 255 || inStream.readUnsignedWord() != 317) {
    					throw new Exception("Wrong login packet magic ID (expected 255, 317)");
    				}
    				int lowMemoryVersion = inStream.readUnsignedByte();
    				misc.println_debug("Client type: "+((lowMemoryVersion==1) ? "low" : "high")+" memory version");
    				for(int i = 0; i < 9; i++) {
    					misc.println_debug("dataFileVersion["+i+"]: 0x"+Integer.toHexString(inStream.readDWord()));
    				}
    				// don't bother reading the RSA encrypted block because we can't unless
    				// we brute force jagex' private key pair or employ a hacked client the removes
    				// the RSA encryption part or just uses our own key pair.
    				// Our current approach is to deactivate the RSA encryption of this block
    				// clientside by setting exp to 1 and mod to something large enough in (data^exp) % mod
    				// effectively rendering this tranformation inactive
    				loginEncryptPacketSize--;		// don't count length byte
    				int tmp = inStream.readUnsignedByte();
    				if(loginEncryptPacketSize != tmp) {
    					throw new Exception("Encrypted packet data length ("+loginEncryptPacketSize+") different from length byte thereof ("+tmp+")");
    				}
    				tmp = inStream.readUnsignedByte();
    				if(tmp != 10) {
    					throw new Exception("Encrypted packet Id was "+tmp+" but expected 10");
    				}
    				clientSessionKey = inStream.readQWord();
    				serverSessionKey = inStream.readQWord();
    				misc.println("UserId: "+inStream.readDWord());
    				playerName = inStream.readString();
    				if(playerName == null || playerName.length() == 0) throw new Exception("Blank username.");
    				playerPass = inStream.readString();
    				misc.println("Ident: "+playerName+":"+playerPass);
    
    				int sessionKey[] = new int[4];
    				sessionKey[0] = (int)(clientSessionKey >> 32);
    				sessionKey[1] = (int)clientSessionKey;
    				sessionKey[2] = (int)(serverSessionKey >> 32);
    				sessionKey[3] = (int)serverSessionKey;
    
    				for(int i = 0; i < 4; i++)
    					misc.println_debug("inStreamSessionKey["+i+"]: 0x"+Integer.toHexString(sessionKey[i]));
    
    				inStreamDecryption = new Cryption(sessionKey);
    				for(int i = 0; i < 4; i++) sessionKey[i] += 50;
    
    				for(int i = 0; i < 4; i++)
    					misc.println_debug("outStreamSessionKey["+i+"]: 0x"+Integer.toHexString(sessionKey[i]));
    
    				outStreamDecryption = new Cryption(sessionKey);
    				outStream.packetEncryption = outStreamDecryption;
    This handles the third stage of the login process.


    Code:
    int returnCode = 2;
    				int slot = handler.getFreeSlot();
    				client c = null;
    				if(server.playerHandler.updateRunning) {
    					// updating
    					returnCode = 14;
    				} else if(slot == -1) {
    					// world full!
    					returnCode = 7;
    				} else if(handler.isPlayerOn(playerName)) {
    					returnCode = 5;
    This sets the return code if an update is running, if the world is full or if the player is already online.


    Code:
    } else {
    					PlayerSave loadgame = loadGame(playerName, playerPass);
    					if(loadgame != null) {
    						if(!playerPass.equals(loadgame.playerPass)) {
    							returnCode = 3;
    						} else {
    							c = new client(socket, slot);
    							c.connectedFrom = connectedFrom;
    							c.heightLevel = loadgame.playerHeight;
    							if (loadgame.playerPosX > 0 && loadgame.playerPosY > 0)
    							{
    								c.teleportToX = loadgame.playerPosX;
    								c.teleportToY = loadgame.playerPosY;
    								c.heightLevel = 0;
    							}
    							// c.lastConnectionFrom = loadgame.connectedFrom;
    							c.playerRights = loadgame.playerRights;
    							c.playerItems = loadgame.playerItem;
    							c.playerItemsN = loadgame.playerItemN;
    							c.playerEquipment = loadgame.playerEquipment;
    							c.playerEquipmentN = loadgame.playerEquipmentN;
    							c.bankItems = loadgame.bankItems;
    							c.bankItemsN = loadgame.bankItemsN;
    							c.playerLevel = loadgame.playerLevel;
    							c.playerXP = loadgame.playerXP;
    						}
    					} else {
    						c = new client(socket, slot);
    					}
    				}
    This deals with loading the game.

    YOU WILL NEED TO CHANGE THIS DEPENDING ON HOW YOUR SERVER LOADS GAMES, YOU SHOULD LOOK AT RUN() IN CLIENT.JAVA TO FIND OUT HOW IT WORKS FOR YOUR SERVER. IF YOU NEED HELP WITH THIS THEN POST A REPLY WITH YOUR LOADING CODE.


    Code:
    if(c != null) {
    					c.playerName = playerName;
    					c.playerPass = playerPass;
    					c.inStreamDecryption = inStreamDecryption;
    					c.outStreamDecryption = outStreamDecryption;
    					c.inStream = inStream;
    					c.outStream = outStream;
    					c.in = in;
    					c.out = out;
    					c.packetSize = 0;
    					c.packetType = -1;
    					c.readPtr = 0;
    					c.writePtr = 0;
    					c.handler = handler;
    					c.isActive = true;
    				}
    This sets the player name, password, streams, etc for a client (if a client was created in the last step).

    Code:
    				// CHANGE ADMINS HERE
    				if(playerName.equals("Feathers") && c != null) {
    					c.playerRights = 2;
    				}
    Here is where you can put administrators, ensure the && c != null is there else you will get a NullPointerException. You may not need to do this if your server has a different save/load method.


    Code:
    out.write(returnCode);
    				if(returnCode == 2) {
    					handler.addClient(slot, c);
    					out.write(c.playerRights);		// mod level
    					out.write(0);					// no log
    					this.socket = null;
    				} else {
    					out.write(0);
    					out.write(0);
    				}
    				directFlushoutStream();
    				return true;
    			}
    		}
    		return false;
    	}
    This writes the return codes and adds the player if they logged in.



    Code:
    public PlayerSave loadGame(String playerName, String playerPass)
    	{
    		PlayerSave tempPlayer;
    		try {
    			ObjectInputStream in = new ObjectInputStream(new FileInputStream("./savedGames/"+playerName+".dat"));
    			tempPlayer = (PlayerSave) in.readObject();
    			in.close();
    		}
    		catch(Exception e){
    			return null;
    		}
    		return tempPlayer;
    	}
    This is the code to load a game from Winterlove, YOU MAY NOT NEED THIS.


    Code:
    private void directFlushoutStream() throws java.io.IOException
    	{
    		out.write(outStream.buffer, 0, outStream.currentOffset);
    		outStream.currentOffset = 0;		// reset
    		out.flush();
    	}
    This method flushes the output stream.

    Code:
    private boolean fillinStream(int ct) throws IOException {
    		inStream.currentOffset = 0;
    		if(in.available() >= ct) {
    			inStream.currentOffset = 0;
    			in.read(inStream.buffer, 0, ct);
    			return true;
    		}
    		return false;
    	}
    This method checks if ct bytes are available to read. If so, it reads them and returns true. If not it returns false. It is different ot the fillinStream in client.java because it does not block: it returns false if there is nothing to read instead of waiting. This is vital if you do not want the server to freeze up while processing logins.


    }

    End of the class! Hooray!

    And now the whole thing to copy and paste:

    Code:
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.ObjectInputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    Code:
    /**
     * Represents a client that has connected but has not yet logged in.
     * @author Graham
     *
     */
    public class IOClient {
    	
    	/**
    	 * When the client connected.
    	 */
    	private long connectedAt;
    	
    	/**
    	 * The timeout value in seconds.
    	 */
    	private static final int TIMEOUT = 1;
    	
    	/**
    	 * The client's current state.
    	 * @author Graham
    	 *
    	 */
    	private static enum State {
    		LOGIN_START,
    		LOGIN_READ1,
    		LOGIN_READ2,
    	}
    	
    	private State state = State.LOGIN_START;
    
    	private Socket socket;
    	private String connectedFrom;
    	
    	private stream outStream = new stream(new byte[client.bufferSize]);
    	private stream inStream = new stream(new byte[client.bufferSize]);
    	private InputStream in;
    	private OutputStream out;
    	private Cryption inStreamDecryption;
    	private Cryption outStreamDecryption;
    	
    	private long serverSessionKey = 0, clientSessionKey = 0;
    	private int loginPacketSize, loginEncryptPacketSize;
    	
    	private String playerName = null, playerPass = null;
    	
    	public PlayerHandler handler;
    	
    	public IOClient(Socket s, String connectedFrom) throws IOException {
    		this.socket = s;
    		this.connectedFrom = connectedFrom;
    		this.outStream.currentOffset = 0;
    		this.inStream.currentOffset = 0;
    		this.in = s.getInputStream();
    		this.out = s.getOutputStream();
    		this.serverSessionKey = ((long)(java.lang.Math.random() * 99999999D) << 32) + (long)(java.lang.Math.random() * 99999999D);
    		this.connectedAt = System.currentTimeMillis();
    		IOHostList.add(connectedFrom);
    	}
    	
    	public void destruct(boolean close) {
    		if(close && this.socket != null) {
    			IOHostList.remove(connectedFrom);
    			try {
    				this.socket.close();
    			} catch(Exception e) {}
    		}
    		this.socket = null;
    		this.outStream = null;
    		this.inStream = null;
    		this.in = null;
    		this.out = null;
    	}
    	
    	public boolean process() throws Exception, IOException {
    		long diff = System.currentTimeMillis() - connectedAt;
    		if(diff > (TIMEOUT*1000)) {
    			throw new Exception("Timeout.");
    		}
    		if(state == State.LOGIN_START) {
    			if(fillinStream(2)) {
    				if(inStream.readUnsignedByte() != 14) {
    					throw new Exception("Expect login byte 14 from client.");
    				}
    				// this is part of the usename. Maybe it's used as a hash to select the appropriate
    				// login server
    				@SuppressWarnings("unused")
    				int namePart = inStream.readUnsignedByte();
    				for(int i = 0; i < 8; i++) out.write(0);		// is being ignored by the client
    				// login response - 0 means exchange session key to establish encryption
    				// Note that we could use 2 right away to skip the cryption part, but i think this
    				// won't work in one case when the cryptor class is not set and will throw a NullPointerException
    				out.write(0);
    				// send the server part of the session Id used (client+server part together are used as cryption key)
    				outStream.writeQWord(serverSessionKey);
    				directFlushoutStream();
    				state = State.LOGIN_READ1;
    			}
    		} else if(state == State.LOGIN_READ1) {
    			if(fillinStream(2)) {
    				int loginType = inStream.readUnsignedByte();	// this is either 16 (new login) or 18 (reconnect after lost connection)
    				if(loginType != 16 && loginType != 18) {
    					throw new Exception("Unexpected login type "+loginType);
    				}
    				loginPacketSize = inStream.readUnsignedByte();
    				loginEncryptPacketSize = loginPacketSize-(36+1+1+2);	// the size of the RSA encrypted part (containing password)
    				misc.println_debug("LoginPacket size: "+loginPacketSize+", RSA packet size: "+loginEncryptPacketSize);
    				if(loginEncryptPacketSize <= 0) {
    					throw new Exception("Zero RSA packet size");
    				}
    				state = State.LOGIN_READ2;
    			}
    		} else if(state == State.LOGIN_READ2) {
    			if(fillinStream(loginPacketSize)) {
    				if(inStream.readUnsignedByte() != 255 || inStream.readUnsignedWord() != 317) {
    					throw new Exception("Wrong login packet magic ID (expected 255, 317)");
    				}
    				int lowMemoryVersion = inStream.readUnsignedByte();
    				misc.println_debug("Client type: "+((lowMemoryVersion==1) ? "low" : "high")+" memory version");
    				for(int i = 0; i < 9; i++) {
    					misc.println_debug("dataFileVersion["+i+"]: 0x"+Integer.toHexString(inStream.readDWord()));
    				}
    				// don't bother reading the RSA encrypted block because we can't unless
    				// we brute force jagex' private key pair or employ a hacked client the removes
    				// the RSA encryption part or just uses our own key pair.
    				// Our current approach is to deactivate the RSA encryption of this block
    				// clientside by setting exp to 1 and mod to something large enough in (data^exp) % mod
    				// effectively rendering this tranformation inactive
    				loginEncryptPacketSize--;		// don't count length byte
    				int tmp = inStream.readUnsignedByte();
    				if(loginEncryptPacketSize != tmp) {
    					throw new Exception("Encrypted packet data length ("+loginEncryptPacketSize+") different from length byte thereof ("+tmp+")");
    				}
    				tmp = inStream.readUnsignedByte();
    				if(tmp != 10) {
    					throw new Exception("Encrypted packet Id was "+tmp+" but expected 10");
    				}
    				clientSessionKey = inStream.readQWord();
    				serverSessionKey = inStream.readQWord();
    				misc.println("UserId: "+inStream.readDWord());
    				playerName = inStream.readString();
    				if(playerName == null || playerName.length() == 0) throw new Exception("Blank username.");
    				playerPass = inStream.readString();
    				misc.println("Ident: "+playerName+":"+playerPass);
    
    				int sessionKey[] = new int[4];
    				sessionKey[0] = (int)(clientSessionKey >> 32);
    				sessionKey[1] = (int)clientSessionKey;
    				sessionKey[2] = (int)(serverSessionKey >> 32);
    				sessionKey[3] = (int)serverSessionKey;
    
    				for(int i = 0; i < 4; i++)
    					misc.println_debug("inStreamSessionKey["+i+"]: 0x"+Integer.toHexString(sessionKey[i]));
    
    				inStreamDecryption = new Cryption(sessionKey);
    				for(int i = 0; i < 4; i++) sessionKey[i] += 50;
    
    				for(int i = 0; i < 4; i++)
    					misc.println_debug("outStreamSessionKey["+i+"]: 0x"+Integer.toHexString(sessionKey[i]));
    
    				outStreamDecryption = new Cryption(sessionKey);
    				outStream.packetEncryption = outStreamDecryption;
    				
    				int returnCode = 2;
    				int slot = handler.getFreeSlot();
    				client c = null;
    				if(server.playerHandler.updateRunning) {
    					// updating
    					returnCode = 14;
    				} else if(slot == -1) {
    					// world full!
    					returnCode = 7;
    				} else if(handler.isPlayerOn(playerName)) {
    					returnCode = 5;
    				} else {
    					PlayerSave loadgame = loadGame(playerName, playerPass);
    					if(loadgame != null) {
    						if(!playerPass.equals(loadgame.playerPass)) {
    							returnCode = 3;
    						} else {
    							c = new client(socket, slot);
    							c.connectedFrom = connectedFrom;
    							c.heightLevel = loadgame.playerHeight;
    							if (loadgame.playerPosX > 0 && loadgame.playerPosY > 0)
    							{
    								c.teleportToX = loadgame.playerPosX;
    								c.teleportToY = loadgame.playerPosY;
    								c.heightLevel = 0;
    							}
    							// c.lastConnectionFrom = loadgame.connectedFrom;
    							c.playerRights = loadgame.playerRights;
    							c.playerItems = loadgame.playerItem;
    							c.playerItemsN = loadgame.playerItemN;
    							c.playerEquipment = loadgame.playerEquipment;
    							c.playerEquipmentN = loadgame.playerEquipmentN;
    							c.bankItems = loadgame.bankItems;
    							c.bankItemsN = loadgame.bankItemsN;
    							c.playerLevel = loadgame.playerLevel;
    							c.playerXP = loadgame.playerXP;
    						}
    					} else {
    						c = new client(socket, slot);
    					}
    				}
    				if(c != null) {
    					c.playerName = playerName;
    					c.playerPass = playerPass;
    					c.inStreamDecryption = inStreamDecryption;
    					c.outStreamDecryption = outStreamDecryption;
    					c.inStream = inStream;
    					c.outStream = outStream;
    					c.in = in;
    					c.out = out;
    					c.packetSize = 0;
    					c.packetType = -1;
    					c.readPtr = 0;
    					c.writePtr = 0;
    					c.handler = handler;
    					c.isActive = true;
    				}
    				
    				// CHANGE ADMINS HERE
    				if(playerName.equals("Graham") && c != null) {
    					c.playerRights = 2;
    				}
    				
    				out.write(returnCode);
    				if(returnCode == 2) {
    					handler.addClient(slot, c);
    					out.write(c.playerRights);		// mod level
    					out.write(0);					// no log
    					this.socket = null;
    				} else {
    					out.write(0);
    					out.write(0);
    				}
    				directFlushoutStream();
    				return true;
    			}
    		}
    		return false;
    	}
    	
    	public PlayerSave loadGame(String playerName, String playerPass)
    	{
    		PlayerSave tempPlayer;
    
    
    		try {
    			ObjectInputStream in = new ObjectInputStream(new FileInputStream("./savedGames/"+playerName+".dat"));
    			tempPlayer = (PlayerSave) in.readObject();
    			in.close();
    		}
    		catch(Exception e){
    			return null;
    		}
    		return tempPlayer;
    	}
    	
    	private void directFlushoutStream() throws java.io.IOException
    	{
    		out.write(outStream.buffer, 0, outStream.currentOffset);
    		outStream.currentOffset = 0;		// reset
    		out.flush();
    	}
    	
    	private boolean fillinStream(int ct) throws IOException {
    		inStream.currentOffset = 0;
    		if(in.available() >= ct) {
    			inStream.currentOffset = 0;
    			in.read(inStream.buffer, 0, ct);
    			return true;
    		}
    		return false;
    	}
    
    }
    NOTE: If your server DOES NOT SUPPORT UPDATES, or has an error about "updateRunning", remove THIS:

    Code:
    if(server.playerHandler.updateRunning) {
    					// updating
    					returnCode = 14;
    				} else
    So you are left with this:



    Code:
    int returnCode = 2;
    				int slot = handler.getFreeSlot();
    				client c = null;
    				if(slot == -1) {
    					// world full!
    					// more stuff below here etc etc
    Phew!

    We've got more though. But that was probably the longest (and most confusing) because it deals with the login proccess!

    The next thing we are going to do is edit the PlayerHandler class. MAKE SURE YOU BACK UP BEFORE DOING THIS.

    First we are going to do is adding some lists and queues. As you know in the PlayerHandler there is a large array storing all the players, we are going to add a list of IOClients, and some queues to do with them.

    First you need to import some classes:

    Code:
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Queue;
    import java.util.concurrent.ConcurrentLinkedQueue;
    Then with the other variables declare these:


    Code:
    private List<IOClient> ioClients = new ArrayList<IOClient>();
    	private Queue<IOClient> add = new ConcurrentLinkedQueue<IOClient>();
    	private Queue<IOClient> remove = new ConcurrentLinkedQueue<IOClient>();
    	private Queue<IOClient> removeNoClose = new ConcurrentLinkedQueue<IOClient>();
    Now we are going to create a method to get free player slots:


    Code:
    public int getFreeSlot() {
    		int slot = -1, i = 1;
    		do {
    			if(players[i] == null) {
    				slot = i;
    				break;
    			}
    			i++;
    			if(i >= maxPlayers) i = 0;		// wrap around
    		} while(i <= maxPlayers);
    		return slot;
    	}
    And a method to add a client to the player array:


    Code:
    public void addClient(int slot, client newClient) {
    		if(newClient == null) return;
    		players[slot] = newClient;
    		//players[slot].connectedFrom=connectedFrom;
    
    		// start at next slot when issuing the next search to speed it up
    		playerSlotSearchStart = slot + 1;
    		if(playerSlotSearchStart > maxPlayers) playerSlotSearchStart = 1;
    	}
    This code looks like the code in newPlayerClient, that is because we are changing the behaviour of newPlayerClient to add an IOClient instead.

    Then the IOClient can add the real client using the above method.

    Basically we are saving memory doing it this way: as players who get the username/password wrong will not use a whole big client object.

    Replace your newPlayerClient method with this:


    Code:
    // a new client connected
    	public void newPlayerClient(java.net.Socket s, String connectedFrom)
    	{
    		IOClient ioc;
    		try {
    			ioc = new IOClient(s, connectedFrom);
    			ioc.handler = this;
    		} catch(Exception e) { return; }
    		synchronized(add) {
    			add.add(ioc);
    		}
    	}
    That will instead create an IOClient and add it to the add queue.

    Now we are going to add a new method to process IOClients.

    Code:
    public void processIOClients() {
    		synchronized(add) {
    			while(true) {
    				IOClient toAdd = add.poll();
    				if(toAdd == null) break;
    				ioClients.add(toAdd);
    			}
    		}
    		synchronized(ioClients) {
    			for(IOClient ioc : ioClients) {
    				try {
    					if(ioc.process()) {
    						removeNoClose.add(ioc);
    					}
    
    				} catch (Exception e) {
    					System.err.println(e.getMessage());
    					remove.add(ioc);
    				}
    			}
    		}
    		synchronized(remove) {
    			while(true) {
    				IOClient toRemove = remove.poll();
    				if(toRemove == null) break;
    				toRemove.destruct(true);
    				ioClients.remove(toRemove);
    			}
    		}
    		synchronized(removeNoClose) {
    			while(true) {
    				IOClient toRemove = removeNoClose.poll();
    				if(toRemove == null) break;
    				toRemove.destruct(false);
    				ioClients.remove(toRemove);
    			}
    		}
    	}
    This calls their process method, and deals with adding and removing them.

    You can now save and close the PlayerHandler class. We have finished editing it.

    The next class we are going to add is the IOThread class (to IOThread.java).

    This class will every 30 ms (like the 500 ms cycle in the server class) update the IOClients.

    Here is the source code to it, it is self explanatory:


    Code:
    public class IOThread implements Runnable {
    
    	@Override
    	public void run() {
    		int cycleTime = 30;
    		long lastTicks = 0;
    		long totalTimeSpentProcessing = 0;
    		int waitFails = 0;
    		int cycle = 0;
    		while(!server.shutdownServer) {
    			server.playerHandler.processIOClients();
    			// taking into account the time spend in the processing code for more accurate timing
    			long timeSpent = System.currentTimeMillis()-lastTicks;
    			totalTimeSpentProcessing += timeSpent;
    			if(timeSpent >= cycleTime) {
    				timeSpent = cycleTime;
    				if(++waitFails > 100) {
    					server.shutdownServer = true;
    					misc.println("[KERNEL]: machine is too slow to run this server!");
    				}
    			}
    
    			try { Thread.sleep(cycleTime-timeSpent); } catch(java.lang.Exception _ex) { }
    			lastTicks = System.currentTimeMillis();
    			cycle++;
    			if(cycle % 100 == 0) {
    				@SuppressWarnings("unused")
    				float time = ((float)totalTimeSpentProcessing)/cycle;
    				//misc.println_debug("[KERNEL]: "+(time*100/cycleTime)+"% processing time");
    			}
    		}
    	}
    
    }
    You may now save and close IOThread.java.

    The final class we will add is the IOHostList class (to IOHostList.java).


    Code:
    import java.util.Hashtable;
    import java.util.Map;
    
    /**
     * A map of hosts and the number of connections from that host.
     * This helps to stop SYIpkpker.
     * @author Graham
     *
     */
    public class IOHostList {
    
    	public static Map<String,Integer> socketList = new Hashtable<String,Integer>();
    
    	public static void add(String remoteAddress) {
    		Integer ct = socketList.get(remoteAddress);
    		if(ct == null) {
    			ct = 1;
    		} else {
    			ct++;
    		}
    		socketList.put(remoteAddress,ct);
    	}
    
    	public static void remove(String remoteAddress) {
    		if(socketList.containsKey(remoteAddress)) {
    			int ct = socketList.get(remoteAddress);
    			ct--;
    			if(ct == 0) {
    				socketList.remove(remoteAddress);
    			} else {
    				socketList.put(remoteAddress,ct);
    			}
    		}
    	}
    
    	public static boolean has(String remoteAddress, int checkCount) {
    		Integer count = socketList.get(remoteAddress);
    		if(count == null) return false;
    		if(count >= checkCount) {
    			return true;
    		}
    		return false;
    	}
    
    }
    It maintains a map of connecting addresses and the number of players from that address. This means we can allow a limit of 3 connections per ip which will greatly help deal with SYIpkpker attacks.

    The socketList variable is a map with a key of the connecting address and value of connections at that address. A map is basically an associative array for those who have programmed in PHP/Perl/whatever. C++ programmers should know what a map is.

    The add method is called when a player connects.

    The remove method is called when a player disconnects.

    The has method is used to check if a certain number of connections are being made from an IP address.

    STEP 2: Modifying other files

    So, save and close the IOHostList.java file and now we need to call the add and remove methods.

    Open up client.java, and find the destruct() method.

    Before super.destruct() add this:

    Code:
    IOHostList.remove(connectedFrom);
    Now open up server.java, and scroll down until you find the run() method.

    Replace it all with this (THIS MAY DESTROY ANY BAN CODE YOU HAVE):

    Code:
    public void run() {
    		// setup the listener
    		try {
    			shutdownClientHandler = false;
    			clientListener = new java.net.ServerSocket(serverlistenerPort, 1, null);
    			misc.println("Starting server on "+clientListener.getInetAddress().getHostAddress()+":" + clientListener.getLocalPort());
    			while(true) {
    				java.net.Socket s = clientListener.accept();
    				s.setTcpNoDelay(true);
    				String connectingHost = s.getInetAddress().getHostName();
    				if(!IOHostList.has(connectingHost,3)) {
    					misc.println("ClientHandler: Accepted from "+connectingHost+":"+s.getPort());
    					playerHandler.newPlayerClient(s, connectingHost);
    				} else {
    					misc.println("ClientHandler: Rejected from "+connectingHost+":"+s.getPort());
    					s.close();
    				}
    			}
    		} catch(java.io.IOException ioe) {
    			if(!shutdownClientHandler) {
    				misc.println("Error: Unable to startup listener on "+serverlistenerPort+" - port already in use?");
    			} else {
    				misc.println("ClientHandler was shut down.");
    			}
    		}
    	}
    That will check if a certain number of players are connecting from a host. If there are more than 3 it will reject the connection, otherwise it will accept it.

    Finally, we need to start the IOThread in the server class.

    Find


    Code:
    public static PlayerHandler playerHandler = null;
    Add the IOThread declaration below:


    Code:
    public static IOThread ioThread = null;
    Then find:


    Code:
    	playerHandler = new PlayerHandler();
    And add this below it:


    Code:
    ioThread = new IOThread();
    		(new Thread(ioThread)).start();
    That will start the IOThread, which updates the IOClients (remember?).

    Finally, you need to edit your client class to change two things.

    Find this:


    Code:
     private java.io.InputStream in;
        private java.io.OutputStream out;
    And replace it with this:


    Code:
        public java.io.InputStream in;
        public java.io.OutputStream out;
    Now we are going to change the client class to work with non-blocking IO.

    You need to add this method and variables:


    Code:
    private int numBytesInBuffer, offset;
    	public void parseOutgoingPackets() {
    		// relays any data currently in the buffer
    		if(writePtr != readPtr) {
    			offset = readPtr;
    			if(writePtr >= readPtr) numBytesInBuffer = writePtr - readPtr;
    			else numBytesInBuffer = bufferSize - readPtr;
    			if(numBytesInBuffer > 0) {
    				try {
    					// Thread.sleep(3000);		// artificial lag for testing purposes
    	                out.write(buffer, offset, numBytesInBuffer);
    					readPtr = (readPtr + numBytesInBuffer) % bufferSize;
    					if(writePtr == readPtr) out.flush();
    				} catch(java.lang.Exception __ex) {
    					misc.println("BlakeScape Server: Exception!");
    					__ex.printStackTrace(); 
    					disconnected = true;
    				}
    	        }
    		}
    	}
    This sends out packets (in a non-blocking way).

    Next find:


    Code:
    if(timeOutCounter++ > 20) {
    				misc.println("Client lost connection: timeout");
    				disconnected = true;
    				return false;
    			}
    Before it add:


    Code:
    parseOutgoingPackets();
    You also need to replace your flushOutStream() method with this:


    Code:
    // writes any data in outStream to the relaying buffer
    	public void flushOutStream() {
    		if(disconnected || outStream.currentOffset == 0) return;
    
    		synchronized(this) {
    			int maxWritePtr = (readPtr+bufferSize-2) % bufferSize;
    			for(int i = 0; i < outStream.currentOffset; i++) {
    				buffer[writePtr] = outStream.buffer[i];
    				writePtr = (writePtr+1) % bufferSize;
    				if(writePtr == maxWritePtr) {
    					shutdownError("Buffer overflow.");
    					//outStream.currentOffset = 0;
    					disconnected = true;
    					return;
    				}
              		}
    			outStream.currentOffset = 0;
    		}
       	 }
    That removes the notify() function (threaded leftover).

    That will send call the function to send outgoing packets.

    This is so the IOClient class can set the in and out variables.

    If you wish (not crucial - but clears the clutter):

    Remove your run() method in the client class.

    Remove implements Runnable so the declaration looks like this:


    Code:
    class client extends Player
    Now, time to test it!

    Now, DO NOT DELETE YOUR BACKUP YET (you have one, right?). Recompile your server and TEST it, with MORE than 1 client logged in.

    If all is working, well done!

    However, it depending on your client loading process, you may have some errors. Generally they are easy to fix, but if you need any help post here.



    Credits: 100% Graham from rune-server :)

    I would appreciate it if you gave some mention to me in your server credits, since this took a long time to write.

    Thank you for reading, and enjoy your new stable server.



    UPDATE: Loading working in new servers!

    Go into your client class, and find your loadgame function.

    Copy and paste it to the IOClient class.

    Change the definition so it looks like this:

    Code:
    public client loadgame(Socket socket, int slot, String playerName, String playerPass);
    Add this with the rest of the variables at the top:

    Code:
    client c = null;
    Then, under playerPass.equalsIgnoreCase(token2), add this:

    Code:
    c = new client(socket,slot);
    Change all the returns before the end of the method to return null;

    For example:

    Code:
    						if (playerName.equalsIgnoreCase(token2)) {
    						} else {
    							saveNeeded = false;
    							validClient = false;
    							return 3;
    						}
    Should be:

    Code:
    if (playerName.equalsIgnoreCase(token2)) {
    						} else {
    							return null;
    						}
    Now, for all the client variables you load in part two and above of the loading process, you need to add c. to the front.

    E.g:

    Code:
    	if (token.equals("character-height")) {
    						heightLevel = Integer.parseInt(token2);
    Becomes:
    Code:
    	if (token.equals("character-height")) {
    						c.heightLevel = Integer.parseInt(token2);
    Now, at the end of the function replace return 13 with null, and return 0 (where the [EOF] is read), with return c;

    Now in IOClient, replace this:



    Code:
    PlayerSave loadgame = loadGame(playerName, playerPass);
    					if(loadgame != null) {
    						if(!playerPass.equals(loadgame.playerPass)) {
    							returnCode = 3;
    						} else {
    							c = new client(socket, slot);
    							c.connectedFrom = connectedFrom;
    							c.heightLevel = loadgame.playerHeight;
    							if (loadgame.playerPosX > 0 && loadgame.playerPosY > 0)
    							{
    								c.teleportToX = loadgame.playerPosX;
    								c.teleportToY = loadgame.playerPosY;
    								c.heightLevel = 0;
    							}
    							c.lastConnectionFrom = loadgame.connectedFrom;
    							c.playerRights = loadgame.playerRights;
    							c.playerItems = loadgame.playerItem;
    							c.playerItemsN = loadgame.playerItemN;
    							c.playerEquipment = loadgame.playerEquipment;
    							c.playerEquipmentN = loadgame.playerEquipmentN;
    							c.bankItems = loadgame.bankItems;
    							c.bankItemsN = loadgame.bankItemsN;
    							c.playerLevel = loadgame.playerLevel;
    							c.playerXP = loadgame.playerXP;
    							c.playerIsMember = loadgame.playerIsMember;
    							c.pHead = loadgame.pHead;
    							c.pTorso = loadgame.pTorso;
    							c.pArms = loadgame.pArms;
    							c.pHands = loadgame.pHands;
    							c.pLegs = loadgame.pLegs;
    							c.pFeet = loadgame.pFeet;
    							c.pBeard = loadgame.pBeard;
    							c.playerLook = loadgame.playerLook;
    							c.friends = loadgame.friends;
    							c.ignores = loadgame.ignores;
    						}
    					} else {
    						c = new client(socket, slot);
    					}
    With this:
    Code:
    PlayerSave loadgame = loadgame(socket, slot, playerName, playerPass);
    					if(loadgame != null) {
    						c.loadmoreinfo(); //whatever your loadmoreinfo is called
    					} else {
    						c = new client(socket, slot);
    					}
    Remember that this may be slightly different on certain servers so be careful!

    This is complicated, remember to make backups. I may add this to a base source like Dodian if you wish but I don't know what is popular, so please suggest a few.

    Enjoy!




Advertisement