using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; namespace MinecraftClient { /// /// The class containing all the core functions needed to communicate with a Minecraft server. /// public class MinecraftCom : IAutoComplete { #region Login to Minecraft.net, Obtaining a session ID public enum LoginResult { Error, Success, WrongPassword, Blocked, AccountMigrated, NotPremium }; /// /// Allows to login to a premium Minecraft account, and retrieve the session ID. /// /// Login /// Password /// Will contain the data returned by Minecraft.net, if the login is successful : Version:UpdateTicket:Username:SessionID /// Returns the status of the login (Success, Failure, etc.) public static LoginResult GetLogin(string user, string pass, ref string outdata) { try { Console.ForegroundColor = ConsoleColor.DarkGray; WebClient wClient = new WebClient(); Console.WriteLine("https://login.minecraft.net/?user=" + user + "&password=<******>&version=13"); string result = wClient.DownloadString("https://login.minecraft.net/?user=" + user + "&password=" + pass + "&version=13"); outdata = result; Console.WriteLine(result); Console.ForegroundColor = ConsoleColor.Gray; if (result == "Bad login") { return LoginResult.WrongPassword; } if (result == "User not premium") { return LoginResult.NotPremium; } if (result == "Too many failed logins") { return LoginResult.Blocked; } if (result == "Account migrated, use e-mail as username.") { return LoginResult.AccountMigrated; } else return LoginResult.Success; } catch (WebException) { return LoginResult.Error; } } #endregion #region Keep-Alive for a Minecraft.net session, should be called every 5 minutes (currently unused) /// /// The session ID will expire within 5 minutes unless this function is called every 5 minutes /// /// Username /// Session ID to keep alive public static void SessionKeepAlive(string user, string sessionID) { new WebClient().DownloadString("https://login.minecraft.net/session?name=" + user + "&session=" + sessionID); } #endregion #region Session checking when joining a server in online mode /// /// This method allows to join an online-mode server. /// It Should be called between the handshake and the login attempt. /// /// Username /// A valid session ID for this username /// Hash returned by the server during the handshake /// Returns true if the check was successful public static bool SessionCheck(string user, string sessionID, string hash) { Console.ForegroundColor = ConsoleColor.DarkGray; WebClient client = new WebClient(); Console.Write("http://session.minecraft.net/game/joinserver.jsp?user=" + user + "&sessionId=" + sessionID + "&serverId=" + hash + " ... "); string result = client.DownloadString("http://session.minecraft.net/game/joinserver.jsp?user=" + user + "&sessionId=" + sessionID + "&serverId=" + hash); Console.WriteLine(result); Console.ForegroundColor = ConsoleColor.Gray; return (result == "OK"); } #endregion #region Server-side session checking (programmed for testing purposes) /// /// Reproduces the username checking done by an online-mode server during the login process. /// /// Username /// Hash sent by the server during the handshake /// Returns true if the user is allowed to join the server public static bool ServerSessionCheck(string user, string hash) { Console.ForegroundColor = ConsoleColor.DarkGray; WebClient client = new WebClient(); ConsoleIO.WriteLine("http://session.minecraft.net/game/checkserver.jsp?user=" + user + "&serverId=" + hash); string result = client.DownloadString("http://session.minecraft.net/game/checkserver.jsp?user=" + user + "&serverId=" + hash); ConsoleIO.WriteLine(result); Console.ForegroundColor = ConsoleColor.Gray; return (result == "YES"); } #endregion TcpClient c = new TcpClient(); Crypto.AesStream s; public bool HasBeenKicked { get { return connectionlost; } } bool connectionlost = false; bool encrypted = false; byte protocolversion; public bool Update() { for (int i = 0; i < bots.Count; i++) { bots[i].Update(); } if (c.Client == null || !c.Connected) { return false; } byte id = 0; try { while (c.Client.Available > 0) { id = readNextByte(); ProcessResult result = processPacket(id); //Debug : Print packet IDs that are beign processed. Green = OK, Red = Unknown packet //If the client gets out of sync, check the last green packet processing code. //if (result == ProcessResult.OK) { printstring("§a0x" + id.ToString("X"), false); } //else { printstring("§c0x" + id.ToString("X"), false); } if (result == ProcessResult.ConnectionLost) { return false; } } } catch (SocketException) { return false; } return true; } public void DebugDump() { byte[] cache = new byte[128000]; Receive(cache, 0, 128000, SocketFlags.None); string dump = BitConverter.ToString(cache); System.IO.File.WriteAllText("debug.txt", dump); System.Diagnostics.Process.Start("debug.txt"); } public bool OnConnectionLost() { if (!connectionlost) { connectionlost = true; for (int i = 0; i < bots.Count; i++) { if (bots[i].OnDisconnect(ChatBot.DisconnectReason.ConnectionLost, "Connection has been lost.")) { return true; //The client is about to restart } } } return false; } private enum ProcessResult { OK, ConnectionLost, UnknownPacket } private ProcessResult processPacket(int id) { int nbr = 0; switch (id) { case 0x00: byte[] keepalive = new byte[5] { 0, 0, 0, 0, 0 }; Receive(keepalive, 1, 4, SocketFlags.None); Send(keepalive); break; case 0x01: readData(4); readNextString(); readData(5); break; case 0x02: readData(1); readNextString(); readNextString(); readData(4); break; case 0x03: string message = readNextString(); if (protocolversion >= 72) { //printstring("§8" + message, false); //Debug : Show the RAW JSON data message = ChatParser.ParseText(message); printstring(message, false); } else printstring(message, false); for (int i = 0; i < bots.Count; i++) { bots[i].GetText(message); } break; case 0x04: readData(16); break; case 0x05: readData(6); readNextItemSlot(); break; case 0x06: readData(12); break; case 0x07: readData(9); break; case 0x08: if (protocolversion >= 72) { readData(10); } else readData(8); break; case 0x09: readData(8); readNextString(); break; case 0x0A: readData(1); break; case 0x0B: readData(33); break; case 0x0C: readData(9); break; case 0x0D: readData(41); break; case 0x0E: readData(11); break; case 0x0F: readData(10); readNextItemSlot(); readData(3); break; case 0x10: readData(2); break; case 0x11: readData(14); break; case 0x12: readData(5); break; case 0x13: if (protocolversion >= 72) { readData(9); } else readData(5); break; case 0x14: readData(4); readNextString(); readData(16); readNextEntityMetaData(); break; case 0x16: readData(8); break; case 0x17: readData(19); readNextObjectData(); break; case 0x18: readData(26); readNextEntityMetaData(); break; case 0x19: readData(4); readNextString(); readData(16); break; case 0x1A: readData(18); break; case 0x1B: if (protocolversion >= 72) { readData(10); } break; case 0x1C: readData(10); break; case 0x1D: nbr = (int)readNextByte(); readData(nbr * 4); break; case 0x1E: readData(4); break; case 0x1F: readData(7); break; case 0x20: readData(6); break; case 0x21: readData(9); break; case 0x22: readData(18); break; case 0x23: readData(5); break; case 0x26: readData(5); break; case 0x27: if (protocolversion >= 72) { readData(9); } else readData(8); break; case 0x28: readData(4); readNextEntityMetaData(); break; case 0x29: readData(8); break; case 0x2A: readData(5); break; case 0x2B: readData(8); break; case 0x2C: if (protocolversion >= 72) { readNextEntityProperties(protocolversion); } break; case 0x33: readData(13); nbr = readNextInt(); readData(nbr); break; case 0x34: readData(10); nbr = readNextInt(); readData(nbr); break; case 0x35: readData(12); break; case 0x36: readData(14); break; case 0x37: readData(17); break; case 0x38: readNextChunkBulkData(); break; case 0x3C: readData(28); nbr = readNextInt(); readData(3 * nbr); readData(12); break; case 0x3D: readData(18); break; case 0x3E: readNextString(); readData(17); break; case 0x3F: if (protocolversion > 51) { readNextString(); readData(32); } break; case 0x46: readData(2); break; case 0x47: readData(17); break; case 0x64: readNextWindowData(protocolversion); break; case 0x65: readData(1); break; case 0x66: readData(7); readNextItemSlot(); break; case 0x67: readData(3); readNextItemSlot(); break; case 0x68: readData(1); for (nbr = readNextShort(); nbr > 0; nbr--) { readNextItemSlot(); } break; case 0x69: readData(5); break; case 0x6A: readData(4); break; case 0x6B: readData(2); readNextItemSlot(); break; case 0x6C: readData(2); break; case 0x82: readData(10); readNextString(); readNextString(); readNextString(); readNextString(); break; case 0x83: readData(4); nbr = readNextShort(); readData(nbr); break; case 0x84: readData(11); nbr = readNextShort(); if (nbr > 0) { readData(nbr); } break; case 0x85: if (protocolversion >= 74) { readData(13); } break; case 0xC8: if (readNextInt() == 2022) { printstring("You are dead. Type /reco to respawn & reconnect.", false); } if (protocolversion >= 72) { readData(4); } else readData(1); break; case 0xC9: readNextString(); readData(3); break; case 0xCA: if (protocolversion >= 72) { readData(9); } else readData(3); break; case 0xCB: autocomplete_result = readNextString(); autocomplete_received = true; break; case 0xCC: readNextString(); readData(4); break; case 0xCD: readData(1); break; case 0xCE: if (protocolversion > 51) { readNextString(); readNextString(); readData(1); } break; case 0xCF: if (protocolversion > 51) { readNextString(); readData(1); readNextString(); } readData(4); break; case 0xD0: if (protocolversion > 51) { readData(1); readNextString(); } break; case 0xD1: if (protocolversion > 51) { readNextTeamData(); } break; case 0xFA: readNextString(); nbr = readNextShort(); readData(nbr); break; case 0xFF: string reason = readNextString(); ConsoleIO.WriteLine("Disconnected by Server :"); printstring(reason, true); connectionlost = true; for (int i = 0; i < bots.Count; i++) { bots[i].OnDisconnect(ChatBot.DisconnectReason.InGameKick, reason); } return ProcessResult.ConnectionLost; default: return ProcessResult.UnknownPacket; //unknown packet! } return ProcessResult.OK; //packet has been successfully skipped } private void readData(int offset) { if (offset > 0) { try { byte[] cache = new byte[offset]; Receive(cache, 0, offset, SocketFlags.None); } catch (OutOfMemoryException) { } } } private string readNextString() { ushort length = (ushort)readNextShort(); if (length > 0) { byte[] cache = new byte[length * 2]; Receive(cache, 0, length * 2, SocketFlags.None); string result = Encoding.BigEndianUnicode.GetString(cache); return result; } else return ""; } private byte[] readNextByteArray() { short len = readNextShort(); byte[] data = new byte[len]; Receive(data, 0, len, SocketFlags.None); return data; } private short readNextShort() { byte[] tmp = new byte[2]; Receive(tmp, 0, 2, SocketFlags.None); Array.Reverse(tmp); return BitConverter.ToInt16(tmp, 0); } private int readNextInt() { byte[] tmp = new byte[4]; Receive(tmp, 0, 4, SocketFlags.None); Array.Reverse(tmp); return BitConverter.ToInt32(tmp, 0); } private byte readNextByte() { byte[] result = new byte[1]; Receive(result, 0, 1, SocketFlags.None); return result[0]; } private void readNextItemSlot() { short itemid = readNextShort(); //If slot not empty (item ID != -1) if (itemid != -1) { readData(1); //Item count readData(2); //Item damage short length = readNextShort(); //If length of optional NBT data > 0, read it if (length > 0) { readData(length); } } } private void readNextEntityMetaData() { do { byte[] id = new byte[1]; Receive(id, 0, 1, SocketFlags.None); if (id[0] == 0x7F) { break; } int index = id[0] & 0x1F; int type = id[0] >> 5; switch (type) { case 0: readData(1); break; //Byte case 1: readData(2); break; //Short case 2: readData(4); break; //Int case 3: readData(4); break; //Float case 4: readNextString(); break; //String case 5: readNextItemSlot(); break; //Slot case 6: readData(12); break; //Vector (3 Int) } } while (true); } private void readNextObjectData() { int id = readNextInt(); if (id != 0) { readData(6); } } private void readNextTeamData() { readNextString(); //Internal Name byte mode = readNextByte(); if (mode == 0 || mode == 2) { readNextString(); //Display Name readNextString(); //Prefix readNextString(); //Suffix readData(1); //Friendly Fire } if (mode == 0 || mode == 3 || mode == 4) { short count = readNextShort(); for (int i = 0; i < count; i++) { readNextString(); //Players } } } private void readNextEntityProperties(int protocolversion) { if (protocolversion >= 72) { if (protocolversion >= 74) { //Minecraft 1.6.2 readNextInt(); //Entity ID int count = readNextInt(); for (int i = 0; i < count; i++) { readNextString(); //Property name readData(8); //Property value (Double) short othercount = readNextShort(); readData(25 * othercount); } } else { //Minecraft 1.6.0 / 1.6.1 readNextInt(); //Entity ID int count = readNextInt(); for (int i = 0; i < count; i++) { readNextString(); //Property name readData(8); //Property value (Double) } } } } private void readNextWindowData(int protocolversion) { readData(1); byte windowtype = readNextByte(); readNextString(); readData(1); if (protocolversion > 51) { readData(1); if (protocolversion >= 72 && windowtype == 0xb) { readNextInt(); } } } private void readNextChunkBulkData() { short chunkcount = readNextShort(); int datalen = readNextInt(); readData(1); readData(datalen); readData(12 * (chunkcount)); } private static void setcolor(char c) { switch (c) { case '0': Console.ForegroundColor = ConsoleColor.Gray; break; //Should be Black but Black is non-readable on a black background case '1': Console.ForegroundColor = ConsoleColor.DarkBlue; break; case '2': Console.ForegroundColor = ConsoleColor.DarkGreen; break; case '3': Console.ForegroundColor = ConsoleColor.DarkCyan; break; case '4': Console.ForegroundColor = ConsoleColor.DarkRed; break; case '5': Console.ForegroundColor = ConsoleColor.DarkMagenta; break; case '6': Console.ForegroundColor = ConsoleColor.DarkYellow; break; case '7': Console.ForegroundColor = ConsoleColor.Gray; break; case '8': Console.ForegroundColor = ConsoleColor.DarkGray; break; case '9': Console.ForegroundColor = ConsoleColor.Blue; break; case 'a': Console.ForegroundColor = ConsoleColor.Green; break; case 'b': Console.ForegroundColor = ConsoleColor.Cyan; break; case 'c': Console.ForegroundColor = ConsoleColor.Red; break; case 'd': Console.ForegroundColor = ConsoleColor.Magenta; break; case 'e': Console.ForegroundColor = ConsoleColor.Yellow; break; case 'f': Console.ForegroundColor = ConsoleColor.White; break; case 'r': Console.ForegroundColor = ConsoleColor.White; break; } } private static void printstring(string str, bool acceptnewlines) { if (!String.IsNullOrEmpty(str)) { if (!acceptnewlines) { str = str.Replace('\n', ' '); } string[] subs = str.Split(new char[] { '§' }); if (subs[0].Length > 0) { ConsoleIO.Write(subs[0]); } for (int i = 1; i < subs.Length; i++) { if (subs[i].Length > 0) { setcolor(subs[i][0]); if (subs[i].Length > 1) { ConsoleIO.Write(subs[i].Substring(1, subs[i].Length - 1)); } } } ConsoleIO.Write('\n'); } Console.ForegroundColor = ConsoleColor.Gray; } private bool autocomplete_received = false; private string autocomplete_result = ""; public string AutoComplete(string behindcursor) { if (String.IsNullOrEmpty(behindcursor)) return ""; byte[] autocomplete = new byte[3 + (behindcursor.Length * 2)]; autocomplete[0] = 0xCB; byte[] msglen = BitConverter.GetBytes((short)behindcursor.Length); Array.Reverse(msglen); msglen.CopyTo(autocomplete, 1); byte[] msg = Encoding.BigEndianUnicode.GetBytes(behindcursor); msg.CopyTo(autocomplete, 3); autocomplete_received = false; autocomplete_result = behindcursor; Send(autocomplete); int wait_left = 50; //do not wait more than 5 seconds (50 * 100 ms) while (wait_left > 0 && !autocomplete_received) { System.Threading.Thread.Sleep(100); wait_left--; } string[] results = autocomplete_result.Split((char)0x00); return results[0]; } public void setVersion(byte ver) { protocolversion = ver; } public void setClient(TcpClient n) { c = n; } private void setEncryptedClient(Crypto.AesStream n) { s = n; encrypted = true; } private void Receive(byte[] buffer, int start, int offset, SocketFlags f) { while (c.Client.Available < start + offset) { } if (encrypted) { s.Read(buffer, start, offset); } else c.Client.Receive(buffer, start, offset, f); } private void Send(byte[] buffer) { if (encrypted) { s.Write(buffer, 0, buffer.Length); } else c.Client.Send(buffer); } public static bool GetServerInfo(string serverIP, ref byte protocolversion, ref string version) { try { string host; int port; string[] sip = serverIP.Split(':'); host = sip[0]; if (sip.Length == 1) { port = 25565; } else { try { port = Convert.ToInt32(sip[1]); } catch (FormatException) { port = 25565; } } TcpClient tcp = new TcpClient(host, port); byte[] ping = new byte[2] { 0xfe, 0x01 }; tcp.Client.Send(ping, SocketFlags.None); tcp.Client.Receive(ping, 0, 1, SocketFlags.None); if (ping[0] == 0xff) { MinecraftCom ComTmp = new MinecraftCom(); ComTmp.setClient(tcp); string result = ComTmp.readNextString(); Console.ForegroundColor = ConsoleColor.DarkGray; //Console.WriteLine(result.Replace((char)0x00, ' ')); if (result.Length > 2 && result[0] == '§' && result[1] == '1') { string[] tmp = result.Split((char)0x00); protocolversion = (byte)Int16.Parse(tmp[1]); version = tmp[2]; } else { protocolversion = (byte)39; version = "B1.8.1 - 1.3.2"; } Console.WriteLine("Server version : MC " + version + " (protocol v" + protocolversion + ")."); Console.ForegroundColor = ConsoleColor.Gray; return true; } else { Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine("Unexpected answer from the server (is that a Minecraft server ?)"); Console.ForegroundColor = ConsoleColor.Gray; return false; } } catch { Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine("An error occured while attempting to connect to this IP."); Console.ForegroundColor = ConsoleColor.Gray; return false; } } public bool Handshake(string username, string sessionID, ref string serverID, ref byte[] token, string host, int port) { //array byte[] data = new byte[10 + (username.Length + host.Length) * 2]; //packet id data[0] = (byte)2; //Protocol Version data[1] = protocolversion; //short len byte[] sh = BitConverter.GetBytes((short)username.Length); Array.Reverse(sh); sh.CopyTo(data, 2); //username byte[] bname = Encoding.BigEndianUnicode.GetBytes(username); bname.CopyTo(data, 4); //short len sh = BitConverter.GetBytes((short)host.Length); Array.Reverse(sh); sh.CopyTo(data, 4 + (username.Length * 2)); //host byte[] bhost = Encoding.BigEndianUnicode.GetBytes(host); bhost.CopyTo(data, 6 + (username.Length * 2)); //port sh = BitConverter.GetBytes(port); Array.Reverse(sh); sh.CopyTo(data, 6 + (username.Length * 2) + (host.Length * 2)); Send(data); byte[] pid = new byte[1]; Receive(pid, 0, 1, SocketFlags.None); while (pid[0] == 0xFA) //Skip some early plugin messages { processPacket(pid[0]); Receive(pid, 0, 1, SocketFlags.None); } if (pid[0] == 0xFD) { serverID = readNextString(); byte[] Serverkey_RAW = readNextByteArray(); token = readNextByteArray(); if (serverID == "-") { Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine("Server is in offline mode."); Console.ForegroundColor = ConsoleColor.Gray; return true; //No need to check session or start encryption } else { var PublicServerkey = Crypto.GenerateRSAPublicKey(Serverkey_RAW); var SecretKey = Crypto.GenerateAESPrivateKey(); Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine("Handshake sussessful. (Server ID: " + serverID + ')'); Console.ForegroundColor = ConsoleColor.Gray; return StartEncryption(username, sessionID, token, serverID, PublicServerkey, SecretKey); } } else return false; } public bool StartEncryption(string username, string sessionID, byte[] token, string serverIDhash, java.security.PublicKey serverKey, javax.crypto.SecretKey secretKey) { Console.ForegroundColor = ConsoleColor.DarkGray; ConsoleIO.WriteLine("Crypto keys & hash generated."); Console.ForegroundColor = ConsoleColor.Gray; if (serverIDhash != "-") { Console.WriteLine("Checking Session..."); if (!SessionCheck(username, sessionID, new java.math.BigInteger(Crypto.getServerHash(serverIDhash, serverKey, secretKey)).toString(16))) { return false; } } //Encrypt the data byte[] key_enc = Crypto.Encrypt(serverKey, secretKey.getEncoded()); byte[] token_enc = Crypto.Encrypt(serverKey, token); byte[] keylen = BitConverter.GetBytes((short)key_enc.Length); byte[] tokenlen = BitConverter.GetBytes((short)token_enc.Length); Array.Reverse(keylen); Array.Reverse(tokenlen); //Building the packet byte[] data = new byte[5 + (short)key_enc.Length + (short)token_enc.Length]; data[0] = 0xFC; keylen.CopyTo(data, 1); key_enc.CopyTo(data, 3); tokenlen.CopyTo(data, 3 + (short)key_enc.Length); token_enc.CopyTo(data, 5 + (short)key_enc.Length); //Send it back Send(data); //Getting the next packet byte[] pid = new byte[1]; Receive(pid, 0, 1, SocketFlags.None); if (pid[0] == 0xFC) { readData(4); setEncryptedClient(Crypto.SwitchToAesMode(c.GetStream(), secretKey)); return true; } else return false; } public bool FinalizeLogin() { Send(new byte[] { 0xCD, 0 }); try { byte[] pid = new byte[1]; try { if (c.Connected) { Receive(pid, 0, 1, SocketFlags.None); while (pid[0] >= 0xC0 && pid[0] != 0xFF) //Skip some early packets or plugin messages { processPacket(pid[0]); Receive(pid, 0, 1, SocketFlags.None); } if (pid[0] == (byte)1) { readData(4); readNextString(); readData(5); return true; //The Server accepted the request } else if (pid[0] == (byte)0xFF) { string reason = readNextString(); Console.WriteLine("Login rejected by Server :"); printstring(reason, true); for (int i = 0; i < bots.Count; i++) { bots[i].OnDisconnect(ChatBot.DisconnectReason.LoginRejected, reason); } return false; } } } catch { //Connection failed return false; } } catch { //Network error Console.WriteLine("Connection Lost."); return false; } return false; //Login was unsuccessful (received a kick...) } public bool SendChatMessage(string message) { if (String.IsNullOrEmpty(message)) return true; try { byte[] chat = new byte[3 + (message.Length * 2)]; chat[0] = (byte)3; byte[] msglen; msglen = BitConverter.GetBytes((short)message.Length); Array.Reverse(msglen); msglen.CopyTo(chat, 1); byte[] msg; msg = Encoding.BigEndianUnicode.GetBytes(message); msg.CopyTo(chat, 3); Send(chat); return true; } catch (SocketException) { return false; } } public bool SendRespawnPacket() { try { Send(new byte[] { 0xCD, 1 }); return true; } catch (SocketException) { return false; } } public void Disconnect(string message) { if (message == null) message = ""; try { byte[] reason = new byte[3 + (message.Length * 2)]; reason[0] = (byte)0xff; byte[] msglen; msglen = BitConverter.GetBytes((short)message.Length); Array.Reverse(msglen); msglen.CopyTo(reason, 1); if (message.Length > 0) { byte[] msg; msg = Encoding.BigEndianUnicode.GetBytes(message); msg.CopyTo(reason, 3); } Send(reason); } catch (SocketException) { } catch (System.IO.IOException) { } } private List bots = new List(); public void BotLoad(ChatBot b) { b.SetHandler(this); bots.Add(b); b.Initialize(); Settings.SingleCommand = ""; } public void BotUnLoad(ChatBot b) { bots.RemoveAll(item => object.ReferenceEquals(item, b)); } public void BotClear() { bots.Clear(); } } }