Merge API into master. This marks the dawn of a new Bungee era, but must be regarded as UNSTABLE.
This commit is contained in:
84
proxy/pom.xml
Normal file
84
proxy/pom.xml
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-parent</artifactId>
|
||||
<version>1.4.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-proxy</artifactId>
|
||||
<version>1.4.7-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>BungeeCord-Proxy</name>
|
||||
<description>Proxy component of the Elastic Portal Suite</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>mendax</artifactId>
|
||||
<version>1.4.6-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.md-5</groupId>
|
||||
<artifactId>bungeecord-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.47</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>BungeeCord</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>2.4</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Main-Class>net.md_5.bungee.BungeeCord</Main-Class>
|
||||
<Implementation-Version>${describe}</Implementation-Version>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>2.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>**/*.java</exclude>
|
||||
<exclude>**/*.properties</exclude>
|
||||
<exclude>**/*.SF</exclude>
|
||||
<exclude>**/*.DSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<minimizeJar>true</minimizeJar>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
325
proxy/src/main/java/net/md_5/bungee/BungeeCord.java
Normal file
325
proxy/src/main/java/net/md_5/bungee/BungeeCord.java
Normal file
@@ -0,0 +1,325 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.Socket;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.Synchronized;
|
||||
import static net.md_5.bungee.Logger.$;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.ReconnectHandler;
|
||||
import net.md_5.bungee.api.TabListHandler;
|
||||
import net.md_5.bungee.api.config.ConfigurationAdapter;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.connection.Server;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.api.plugin.PluginManager;
|
||||
import net.md_5.bungee.command.*;
|
||||
import net.md_5.bungee.config.YamlConfig;
|
||||
import net.md_5.bungee.packet.DefinedPacket;
|
||||
import net.md_5.bungee.packet.PacketFAPluginMessage;
|
||||
|
||||
/**
|
||||
* Main BungeeCord proxy class.
|
||||
*/
|
||||
public class BungeeCord extends ProxyServer
|
||||
{
|
||||
|
||||
/**
|
||||
* Server protocol version.
|
||||
*/
|
||||
public static final int PROTOCOL_VERSION = 51;
|
||||
/**
|
||||
* Server game version.
|
||||
*/
|
||||
public static final String GAME_VERSION = "1.4.6";
|
||||
/**
|
||||
* Current operation state.
|
||||
*/
|
||||
public volatile boolean isRunning;
|
||||
/**
|
||||
* Configuration.
|
||||
*/
|
||||
public final Configuration config = new Configuration();
|
||||
/**
|
||||
* Thread pool.
|
||||
*/
|
||||
public final ExecutorService threadPool = Executors.newCachedThreadPool();
|
||||
/**
|
||||
* locations.yml save thread.
|
||||
*/
|
||||
private final Timer saveThread = new Timer("Reconnect Saver");
|
||||
/**
|
||||
* Server socket listener.
|
||||
*/
|
||||
private Collection<ListenThread> listeners = new HashSet<>();
|
||||
/**
|
||||
* Fully qualified connections.
|
||||
*/
|
||||
public Map<String, UserConnection> connections = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* Tab list handler
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public TabListHandler tabListHandler;
|
||||
/**
|
||||
* Plugin manager.
|
||||
*/
|
||||
@Getter
|
||||
public final PluginManager pluginManager = new PluginManager();
|
||||
@Getter
|
||||
@Setter
|
||||
private ReconnectHandler reconnectHandler;
|
||||
@Getter
|
||||
@Setter
|
||||
private ConfigurationAdapter configurationAdapter = new YamlConfig();
|
||||
private final Collection<String> pluginChannels = new HashSet<>();
|
||||
|
||||
|
||||
{
|
||||
getPluginManager().registerCommand(new CommandReload());
|
||||
getPluginManager().registerCommand(new CommandEnd());
|
||||
getPluginManager().registerCommand(new CommandList());
|
||||
getPluginManager().registerCommand(new CommandServer());
|
||||
getPluginManager().registerCommand(new CommandIP());
|
||||
getPluginManager().registerCommand(new CommandAlert());
|
||||
getPluginManager().registerCommand(new CommandBungee());
|
||||
|
||||
registerChannel("BungeeCord::Disconnect");
|
||||
registerChannel("BungeeCord::Connect");
|
||||
registerChannel("BungeeCord::Forward");
|
||||
}
|
||||
|
||||
public static BungeeCord getInstance()
|
||||
{
|
||||
return (BungeeCord) ProxyServer.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new instance of BungeeCord.
|
||||
*
|
||||
* @param args command line arguments, currently none are used
|
||||
* @throws IOException when the server cannot be started
|
||||
*/
|
||||
public static void main(String[] args) throws IOException
|
||||
{
|
||||
BungeeCord bungee = new BungeeCord();
|
||||
ProxyServer.setInstance(bungee);
|
||||
$().info("Enabled BungeeCord version " + bungee.getVersion());
|
||||
bungee.start();
|
||||
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
|
||||
while (bungee.isRunning)
|
||||
{
|
||||
String line = br.readLine();
|
||||
if (line != null)
|
||||
{
|
||||
boolean handled = getInstance().getPluginManager().dispatchCommand(ConsoleCommandSender.getInstance(), line);
|
||||
if (!handled)
|
||||
{
|
||||
System.err.println("Command not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start this proxy instance by loading the configuration, plugins and
|
||||
* starting the connect thread.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public void start() throws IOException
|
||||
{
|
||||
config.load();
|
||||
reconnectHandler = new YamlReconnectHandler();
|
||||
isRunning = true;
|
||||
|
||||
File plugins = new File("plugins");
|
||||
plugins.mkdir();
|
||||
pluginManager.loadPlugins(plugins);
|
||||
|
||||
for (ListenerInfo info : config.getListeners())
|
||||
{
|
||||
$().info("Listening on " + info.getHost());
|
||||
ListenThread listener = new ListenThread(info);
|
||||
listener.start();
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
saveThread.scheduleAtFixedRate(new TimerTask()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
getReconnectHandler().save();
|
||||
}
|
||||
}, 0, TimeUnit.MINUTES.toMillis(5));
|
||||
|
||||
new Metrics().start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
this.isRunning = false;
|
||||
$().info("Disabling plugins");
|
||||
for (Plugin plugin : pluginManager.getPlugins())
|
||||
{
|
||||
plugin.onDisable();
|
||||
}
|
||||
|
||||
for (ListenThread listener : listeners)
|
||||
{
|
||||
$().log(Level.INFO, "Closing listen thread {0}", listener.socket);
|
||||
try
|
||||
{
|
||||
listener.socket.close();
|
||||
listener.join();
|
||||
} catch (InterruptedException | IOException ex)
|
||||
{
|
||||
$().severe("Could not close listen thread");
|
||||
}
|
||||
}
|
||||
|
||||
$().info("Closing pending connections");
|
||||
threadPool.shutdown();
|
||||
|
||||
$().info("Disconnecting " + connections.size() + " connections");
|
||||
for (UserConnection user : connections.values())
|
||||
{
|
||||
user.disconnect("Proxy restarting, brb.");
|
||||
}
|
||||
|
||||
$().info("Saving reconnect locations");
|
||||
reconnectHandler.save();
|
||||
saveThread.cancel();
|
||||
|
||||
$().info("Thank you and goodbye");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Miscellaneous method to set options on a socket based on those in the
|
||||
* configuration.
|
||||
*
|
||||
* @param socket to set the options on
|
||||
* @throws IOException when the underlying set methods thrown an exception
|
||||
*/
|
||||
public void setSocketOptions(Socket socket) throws IOException
|
||||
{
|
||||
socket.setSoTimeout(config.getTimeout());
|
||||
socket.setTrafficClass(0x18);
|
||||
socket.setTcpNoDelay(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a packet to all clients that is connected to this instance.
|
||||
*
|
||||
* @param packet the packet to send
|
||||
*/
|
||||
public void broadcast(DefinedPacket packet)
|
||||
{
|
||||
for (UserConnection con : connections.values())
|
||||
{
|
||||
con.packetQueue.add(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return "BungeeCord";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion()
|
||||
{
|
||||
return (BungeeCord.class.getPackage().getImplementationVersion() == null) ? "unknown" : BungeeCord.class.getPackage().getImplementationVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger()
|
||||
{
|
||||
return $();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked") // TODO: Abstract more
|
||||
public Collection<ProxiedPlayer> getPlayers()
|
||||
{
|
||||
return (Collection) connections.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProxiedPlayer getPlayer(String name)
|
||||
{
|
||||
return connections.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Server getServer(String name)
|
||||
{
|
||||
Collection<ProxiedPlayer> users = getServers().get(name).getPlayers();
|
||||
return (users != null && !users.isEmpty()) ? users.iterator().next().getServer() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ServerInfo> getServers()
|
||||
{
|
||||
return config.getServers();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized("pluginChannels")
|
||||
public void registerChannel(String channel)
|
||||
{
|
||||
pluginChannels.add(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized("pluginChannels")
|
||||
public void unregisterChannel(String channel)
|
||||
{
|
||||
pluginChannels.remove(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized("pluginChannels")
|
||||
public Collection<String> getChannels()
|
||||
{
|
||||
return Collections.unmodifiableCollection(pluginChannels);
|
||||
}
|
||||
|
||||
public PacketFAPluginMessage registerChannels()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s : getChannels())
|
||||
{
|
||||
sb.append(s);
|
||||
sb.append('\00');
|
||||
}
|
||||
byte[] payload = sb.substring(0, sb.length() - 1).getBytes();
|
||||
return new PacketFAPluginMessage("REGISTER", payload);
|
||||
}
|
||||
}
|
138
proxy/src/main/java/net/md_5/bungee/EncryptionUtil.java
Normal file
138
proxy/src/main/java/net/md_5/bungee/EncryptionUtil.java
Normal file
@@ -0,0 +1,138 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import net.md_5.bungee.packet.PacketFCEncryptionResponse;
|
||||
import net.md_5.bungee.packet.PacketFDEncryptionRequest;
|
||||
import org.bouncycastle.crypto.BufferedBlockCipher;
|
||||
import org.bouncycastle.crypto.engines.AESFastEngine;
|
||||
import org.bouncycastle.crypto.modes.CFBBlockCipher;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.bouncycastle.crypto.params.ParametersWithIV;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
/**
|
||||
* Class containing all encryption related methods for the proxy.
|
||||
*/
|
||||
public class EncryptionUtil
|
||||
{
|
||||
|
||||
private static final Random random = new Random();
|
||||
private static KeyPair keys;
|
||||
private static SecretKey secret = new SecretKeySpec(new byte[16], "AES");
|
||||
|
||||
static
|
||||
{
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public static PacketFDEncryptionRequest encryptRequest() throws NoSuchAlgorithmException
|
||||
{
|
||||
if (keys == null)
|
||||
{
|
||||
keys = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
||||
}
|
||||
|
||||
String hash = Long.toString(random.nextLong(), 16);
|
||||
byte[] pubKey = keys.getPublic().getEncoded();
|
||||
byte[] verify = new byte[4];
|
||||
random.nextBytes(verify);
|
||||
return new PacketFDEncryptionRequest(hash, pubKey, verify);
|
||||
}
|
||||
|
||||
public static SecretKey getSecret(PacketFCEncryptionResponse resp, PacketFDEncryptionRequest request) throws BadPaddingException, IllegalBlockSizeException, IllegalStateException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance("RSA");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keys.getPrivate());
|
||||
byte[] decrypted = cipher.doFinal(resp.verifyToken);
|
||||
|
||||
if (!Arrays.equals(request.verifyToken, decrypted))
|
||||
{
|
||||
throw new IllegalStateException("Key pairs do not match!");
|
||||
}
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, keys.getPrivate());
|
||||
byte[] shared = resp.sharedSecret;
|
||||
byte[] secret = cipher.doFinal(shared);
|
||||
|
||||
return new SecretKeySpec(secret, "AES");
|
||||
}
|
||||
|
||||
public static boolean isAuthenticated(String username, String connectionHash, SecretKey shared) throws NoSuchAlgorithmException, IOException
|
||||
{
|
||||
String encName = URLEncoder.encode(username, "UTF-8");
|
||||
|
||||
MessageDigest sha = MessageDigest.getInstance("SHA-1");
|
||||
for (byte[] bit : new byte[][]
|
||||
{
|
||||
connectionHash.getBytes("ISO_8859_1"), shared.getEncoded(), keys.getPublic().getEncoded()
|
||||
})
|
||||
{
|
||||
sha.update(bit);
|
||||
}
|
||||
|
||||
String encodedHash = URLEncoder.encode(new BigInteger(sha.digest()).toString(16), "UTF-8");
|
||||
String authURL = "http://session.minecraft.net/game/checkserver.jsp?user=" + encName + "&serverId=" + encodedHash;
|
||||
String reply;
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(new URL(authURL).openStream())))
|
||||
{
|
||||
reply = in.readLine();
|
||||
}
|
||||
|
||||
return "YES".equals(reply);
|
||||
}
|
||||
|
||||
public static BufferedBlockCipher getCipher(boolean forEncryption, Key shared)
|
||||
{
|
||||
BufferedBlockCipher cip = new BufferedBlockCipher(new CFBBlockCipher(new AESFastEngine(), 8));
|
||||
cip.init(forEncryption, new ParametersWithIV(new KeyParameter(shared.getEncoded()), shared.getEncoded()));
|
||||
return cip;
|
||||
}
|
||||
|
||||
public static SecretKey getSecret()
|
||||
{
|
||||
return secret;
|
||||
}
|
||||
|
||||
public static PublicKey getPubkey(PacketFDEncryptionRequest request) throws InvalidKeySpecException, NoSuchAlgorithmException
|
||||
{
|
||||
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(request.publicKey));
|
||||
}
|
||||
|
||||
public static byte[] encrypt(Key key, byte[] b) throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException
|
||||
{
|
||||
Cipher hasher = Cipher.getInstance("RSA");
|
||||
hasher.init(Cipher.ENCRYPT_MODE, key);
|
||||
return hasher.doFinal(b);
|
||||
}
|
||||
|
||||
public static byte[] getShared(SecretKey key, PublicKey pubkey) throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance("RSA");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, pubkey);
|
||||
return cipher.doFinal(key.getEncoded());
|
||||
}
|
||||
}
|
156
proxy/src/main/java/net/md_5/bungee/EntityMap.java
Normal file
156
proxy/src/main/java/net/md_5/bungee/EntityMap.java
Normal file
@@ -0,0 +1,156 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
/**
|
||||
* Class to rewrite integers within packets.
|
||||
*/
|
||||
public class EntityMap
|
||||
{
|
||||
|
||||
public final static int[][] entityIds = new int[256][];
|
||||
|
||||
static
|
||||
{
|
||||
entityIds[0x05] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x07] = new int[]
|
||||
{
|
||||
1, 5
|
||||
};
|
||||
entityIds[0x11] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x12] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x13] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x14] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x16] = new int[]
|
||||
{
|
||||
1, 5
|
||||
};
|
||||
entityIds[0x17] = new int[]
|
||||
{
|
||||
1, 20
|
||||
};
|
||||
entityIds[0x18] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x19] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x1A] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x1C] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x1E] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x1F] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x20] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x21] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x22] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x23] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x26] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x27] = new int[]
|
||||
{
|
||||
1, 5
|
||||
};
|
||||
entityIds[0x28] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x29] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x2A] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
entityIds[0x37] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
|
||||
entityIds[0x47] = new int[]
|
||||
{
|
||||
1
|
||||
};
|
||||
}
|
||||
|
||||
public static void rewrite(byte[] packet, int oldId, int newId)
|
||||
{
|
||||
int packetId = Util.getId(packet);
|
||||
if (packetId == 0x1D)
|
||||
{ // bulk entity
|
||||
for (int pos = 2; pos < packet.length; pos += 4)
|
||||
{
|
||||
if (oldId == readInt(packet, pos))
|
||||
{
|
||||
setInt(packet, pos, newId);
|
||||
}
|
||||
}
|
||||
} else
|
||||
{
|
||||
int[] idArray = entityIds[packetId];
|
||||
if (idArray != null)
|
||||
{
|
||||
for (int pos : idArray)
|
||||
{
|
||||
if (oldId == readInt(packet, pos))
|
||||
{
|
||||
setInt(packet, pos, newId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setInt(byte[] buf, int pos, int i)
|
||||
{
|
||||
buf[pos] = (byte) (i >> 24);
|
||||
buf[pos + 1] = (byte) (i >> 16);
|
||||
buf[pos + 2] = (byte) (i >> 8);
|
||||
buf[pos + 3] = (byte) i;
|
||||
}
|
||||
|
||||
private static int readInt(byte[] buf, int pos)
|
||||
{
|
||||
return (((buf[pos] & 0xFF) << 24) | ((buf[pos + 1] & 0xFF) << 16) | ((buf[pos + 2] & 0xFF) << 8) | buf[pos + 3] & 0xFF);
|
||||
}
|
||||
}
|
63
proxy/src/main/java/net/md_5/bungee/GenericConnection.java
Normal file
63
proxy/src/main/java/net/md_5/bungee/GenericConnection.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import static net.md_5.bungee.Logger.$;
|
||||
import net.md_5.bungee.packet.PacketFFKick;
|
||||
import net.md_5.bungee.packet.PacketInputStream;
|
||||
|
||||
/**
|
||||
* Class to represent a Minecraft connection.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public class GenericConnection
|
||||
{
|
||||
|
||||
protected final Socket socket;
|
||||
protected final PacketInputStream in;
|
||||
protected final OutputStream out;
|
||||
@Getter
|
||||
public String name;
|
||||
@Getter
|
||||
public String displayName;
|
||||
|
||||
/**
|
||||
* Close the socket with the specified reason.
|
||||
*
|
||||
* @param reason to disconnect
|
||||
*/
|
||||
public void disconnect(String reason)
|
||||
{
|
||||
if (socket.isClosed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
log("disconnected with " + reason);
|
||||
try
|
||||
{
|
||||
out.write(new PacketFFKick("[Proxy] " + reason).getPacket());
|
||||
} catch (IOException ex)
|
||||
{
|
||||
} finally
|
||||
{
|
||||
try
|
||||
{
|
||||
out.flush();
|
||||
out.close();
|
||||
socket.close();
|
||||
} catch (IOException ioe)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void log(String message)
|
||||
{
|
||||
$().info(socket.getInetAddress() + ((name == null) ? " " : " [" + name + "] ") + message);
|
||||
}
|
||||
}
|
176
proxy/src/main/java/net/md_5/bungee/InitialHandler.java
Normal file
176
proxy/src/main/java/net/md_5/bungee/InitialHandler.java
Normal file
@@ -0,0 +1,176 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.crypto.SecretKey;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
import net.md_5.bungee.packet.Packet2Handshake;
|
||||
import net.md_5.bungee.packet.PacketFCEncryptionResponse;
|
||||
import net.md_5.bungee.packet.PacketFDEncryptionRequest;
|
||||
import net.md_5.bungee.packet.PacketFFKick;
|
||||
import net.md_5.bungee.packet.PacketInputStream;
|
||||
import org.bouncycastle.crypto.io.CipherInputStream;
|
||||
import org.bouncycastle.crypto.io.CipherOutputStream;
|
||||
|
||||
public class InitialHandler implements Runnable, PendingConnection
|
||||
{
|
||||
|
||||
private final Socket socket;
|
||||
@Getter
|
||||
private final ListenerInfo listener;
|
||||
private PacketInputStream in;
|
||||
private OutputStream out;
|
||||
private Packet2Handshake handshake;
|
||||
|
||||
public InitialHandler(Socket socket, ListenerInfo info) throws IOException
|
||||
{
|
||||
this.socket = socket;
|
||||
this.listener = info;
|
||||
in = new PacketInputStream(socket.getInputStream());
|
||||
out = socket.getOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] packet = in.readPacket();
|
||||
int id = Util.getId(packet);
|
||||
switch (id)
|
||||
{
|
||||
case 0x02:
|
||||
handshake = new Packet2Handshake(packet);
|
||||
PacketFDEncryptionRequest request = EncryptionUtil.encryptRequest();
|
||||
out.write(request.getPacket());
|
||||
PacketFCEncryptionResponse response = new PacketFCEncryptionResponse(in.readPacket());
|
||||
|
||||
SecretKey shared = EncryptionUtil.getSecret(response, request);
|
||||
if (!EncryptionUtil.isAuthenticated(handshake.username, request.serverId, shared))
|
||||
{
|
||||
throw new KickException("Not authenticated with minecraft.net");
|
||||
}
|
||||
|
||||
// Check for multiple connections
|
||||
ProxiedPlayer old = ProxyServer.getInstance().getPlayer(handshake.username);
|
||||
if (old != null)
|
||||
{
|
||||
old.disconnect("You are already connected to the server");
|
||||
}
|
||||
|
||||
// fire login event
|
||||
LoginEvent event = new LoginEvent(this);
|
||||
if (event.isCancelled())
|
||||
{
|
||||
throw new KickException(event.getCancelReason());
|
||||
}
|
||||
|
||||
out.write(new PacketFCEncryptionResponse().getPacket());
|
||||
in = new PacketInputStream(new CipherInputStream(socket.getInputStream(), EncryptionUtil.getCipher(false, shared)));
|
||||
out = new CipherOutputStream(socket.getOutputStream(), EncryptionUtil.getCipher(true, shared));
|
||||
List<byte[]> customPackets = new ArrayList<>();
|
||||
byte[] custom;
|
||||
while (Util.getId((custom = in.readPacket())) != 0xCD)
|
||||
{
|
||||
customPackets.add(custom);
|
||||
}
|
||||
|
||||
UserConnection userCon = new UserConnection(socket, this, in, out, handshake, customPackets);
|
||||
String server = ProxyServer.getInstance().getReconnectHandler().getServer(userCon);
|
||||
ServerInfo s = BungeeCord.getInstance().config.getServers().get(server);
|
||||
userCon.connect(s);
|
||||
break;
|
||||
case 0xFE:
|
||||
socket.setSoTimeout(100);
|
||||
boolean newPing = false;
|
||||
try
|
||||
{
|
||||
socket.getInputStream().read();
|
||||
newPing = true;
|
||||
} catch (IOException ex)
|
||||
{
|
||||
}
|
||||
Configuration conf = BungeeCord.getInstance().config;
|
||||
String ping = (newPing) ? ChatColor.COLOR_CHAR + "1"
|
||||
+ "\00" + BungeeCord.PROTOCOL_VERSION
|
||||
+ "\00" + BungeeCord.GAME_VERSION
|
||||
+ "\00" + listener.getMotd()
|
||||
+ "\00" + ProxyServer.getInstance().getPlayers().size()
|
||||
+ "\00" + listener.getMaxPlayers()
|
||||
: listener.getMotd() + ChatColor.COLOR_CHAR + ProxyServer.getInstance().getPlayers().size() + ChatColor.COLOR_CHAR + listener.getMaxPlayers();
|
||||
throw new KickException(ping);
|
||||
default:
|
||||
if (id == 0xFA)
|
||||
{
|
||||
run(); // WTF Spoutcraft
|
||||
} else
|
||||
{
|
||||
// throw new IllegalArgumentException("Wasn't ready for packet id " + Util.hex(id));
|
||||
}
|
||||
}
|
||||
} catch (KickException ex)
|
||||
{
|
||||
disconnect(ex.getMessage());
|
||||
} catch (Exception ex)
|
||||
{
|
||||
disconnect("[Proxy Error] " + Util.exception(ex));
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(String reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
out.write(new PacketFFKick(reason).getPacket());
|
||||
} catch (IOException ioe)
|
||||
{
|
||||
} finally
|
||||
{
|
||||
try
|
||||
{
|
||||
out.flush();
|
||||
socket.close();
|
||||
} catch (IOException ioe2)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return (handshake == null) ? null : handshake.username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getVersion()
|
||||
{
|
||||
return (handshake == null) ? -1 : handshake.procolVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getVirtualHost()
|
||||
{
|
||||
return (handshake == null) ? null : new InetSocketAddress(handshake.host, handshake.port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getAddress()
|
||||
{
|
||||
return (InetSocketAddress) socket.getRemoteSocketAddress();
|
||||
}
|
||||
}
|
14
proxy/src/main/java/net/md_5/bungee/KickException.java
Normal file
14
proxy/src/main/java/net/md_5/bungee/KickException.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
/**
|
||||
* Exception, which when thrown will disconnect the player from the proxy with
|
||||
* the specified message.
|
||||
*/
|
||||
public class KickException extends RuntimeException
|
||||
{
|
||||
|
||||
public KickException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
47
proxy/src/main/java/net/md_5/bungee/ListenThread.java
Normal file
47
proxy/src/main/java/net/md_5/bungee/ListenThread.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import static net.md_5.bungee.Logger.$;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
|
||||
/**
|
||||
* Thread to listen and dispatch incoming connections to the proxy.
|
||||
*/
|
||||
public class ListenThread extends Thread
|
||||
{
|
||||
|
||||
public final ServerSocket socket;
|
||||
private final ListenerInfo info;
|
||||
|
||||
public ListenThread(ListenerInfo info) throws IOException
|
||||
{
|
||||
super("Listen Thread - "+ info);
|
||||
this.info = info;
|
||||
socket = new ServerSocket();
|
||||
socket.bind(info.getHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
while (BungeeCord.getInstance().isRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
Socket client = socket.accept();
|
||||
BungeeCord.getInstance().setSocketOptions(client);
|
||||
$().info(client.getInetAddress() + " has connected");
|
||||
InitialHandler handler = new InitialHandler(client,info);
|
||||
BungeeCord.getInstance().threadPool.submit(handler);
|
||||
} catch (SocketException ex)
|
||||
{
|
||||
} catch (IOException ex)
|
||||
{
|
||||
ex.printStackTrace(); // TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
108
proxy/src/main/java/net/md_5/bungee/Logger.java
Normal file
108
proxy/src/main/java/net/md_5/bungee/Logger.java
Normal file
@@ -0,0 +1,108 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.logging.FileHandler;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
/**
|
||||
* Logger to handle formatting and storage of the proxy's logger.
|
||||
*/
|
||||
public class Logger extends java.util.logging.Logger
|
||||
{
|
||||
|
||||
private static final Formatter formatter = new ConsoleLogFormatter();
|
||||
private static final Logger instance = new Logger();
|
||||
|
||||
public Logger()
|
||||
{
|
||||
super("RubberBand", null);
|
||||
try
|
||||
{
|
||||
FileHandler handler = new FileHandler("proxy.log", 1 << 14, 1, true);
|
||||
handler.setFormatter(formatter);
|
||||
addHandler(handler);
|
||||
} catch (IOException ex)
|
||||
{
|
||||
System.err.println("Could not register logger!");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogRecord record)
|
||||
{
|
||||
super.log(record);
|
||||
String message = formatter.format(record);
|
||||
if (record.getLevel() == Level.SEVERE || record.getLevel() == Level.WARNING)
|
||||
{
|
||||
System.err.print(message);
|
||||
} else
|
||||
{
|
||||
System.out.print(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current logger instance.
|
||||
*
|
||||
* @return the current logger instance
|
||||
*/
|
||||
public static Logger $()
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static class ConsoleLogFormatter extends Formatter
|
||||
{
|
||||
|
||||
private SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public String format(LogRecord logrecord)
|
||||
{
|
||||
StringBuilder formatted = new StringBuilder();
|
||||
|
||||
formatted.append(formatter.format(logrecord.getMillis()));
|
||||
Level level = logrecord.getLevel();
|
||||
|
||||
if (level == Level.FINEST)
|
||||
{
|
||||
formatted.append(" [FINEST] ");
|
||||
} else if (level == Level.FINER)
|
||||
{
|
||||
formatted.append(" [FINER] ");
|
||||
} else if (level == Level.FINE)
|
||||
{
|
||||
formatted.append(" [FINE] ");
|
||||
} else if (level == Level.INFO)
|
||||
{
|
||||
formatted.append(" [INFO] ");
|
||||
} else if (level == Level.WARNING)
|
||||
{
|
||||
formatted.append(" [WARNING] ");
|
||||
} else if (level == Level.SEVERE)
|
||||
{
|
||||
formatted.append(" [SEVERE] ");
|
||||
}
|
||||
|
||||
formatted.append(logrecord.getMessage());
|
||||
formatted.append('\n');
|
||||
Throwable throwable = logrecord.getThrown();
|
||||
|
||||
if (throwable != null)
|
||||
{
|
||||
StringWriter writer = new StringWriter();
|
||||
|
||||
throwable.printStackTrace(new PrintWriter(writer));
|
||||
formatted.append(writer);
|
||||
}
|
||||
|
||||
return formatted.toString();
|
||||
}
|
||||
}
|
||||
}
|
144
proxy/src/main/java/net/md_5/bungee/Metrics.java
Normal file
144
proxy/src/main/java/net/md_5/bungee/Metrics.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
import static net.md_5.bungee.Logger.$;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
|
||||
public class Metrics extends Thread
|
||||
{
|
||||
|
||||
/**
|
||||
* The current revision number
|
||||
*/
|
||||
private final static int REVISION = 5;
|
||||
/**
|
||||
* The base url of the metrics domain
|
||||
*/
|
||||
private static final String BASE_URL = "http://mcstats.org";
|
||||
/**
|
||||
* The url used to report a server's status
|
||||
*/
|
||||
private static final String REPORT_URL = "/report/%s";
|
||||
/**
|
||||
* Interval of time to ping (in minutes)
|
||||
*/
|
||||
private final static int PING_INTERVAL = 10;
|
||||
|
||||
public Metrics()
|
||||
{
|
||||
super("Metrics Gathering Thread");
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
boolean firstPost = true;
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
// We use the inverse of firstPost because if it is the first time we are posting,
|
||||
// it is not a interval ping, so it evaluates to FALSE
|
||||
// Each time thereafter it will evaluate to TRUE, i.e PING!
|
||||
postPlugin(!firstPost);
|
||||
|
||||
// After the first post we set firstPost to false
|
||||
// Each post thereafter will be a ping
|
||||
firstPost = false;
|
||||
} catch (IOException ex)
|
||||
{
|
||||
$().info("[Metrics] " + ex.getMessage());
|
||||
}
|
||||
try
|
||||
{
|
||||
sleep(PING_INTERVAL * 1000 * 60);
|
||||
} catch (InterruptedException ex)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method that posts a plugin to the metrics website
|
||||
*/
|
||||
private void postPlugin(boolean isPing) throws IOException
|
||||
{
|
||||
// Construct the post data
|
||||
final StringBuilder data = new StringBuilder();
|
||||
data.append(encode("guid")).append('=').append(encode(BungeeCord.getInstance().config.getUuid()));
|
||||
encodeDataPair(data, "version", ProxyServer.getInstance().getVersion());
|
||||
encodeDataPair(data, "server", "0");
|
||||
encodeDataPair(data, "players", Integer.toString(ProxyServer.getInstance().getPlayers().size()));
|
||||
encodeDataPair(data, "revision", String.valueOf(REVISION));
|
||||
|
||||
// If we're pinging, append it
|
||||
if (isPing)
|
||||
{
|
||||
encodeDataPair(data, "ping", "true");
|
||||
}
|
||||
|
||||
// Create the url
|
||||
URL url = new URL(BASE_URL + String.format(REPORT_URL, encode("BungeeCord")));
|
||||
|
||||
// Connect to the website
|
||||
URLConnection connection;
|
||||
|
||||
connection = url.openConnection();
|
||||
|
||||
connection.setDoOutput(true);
|
||||
final BufferedReader reader;
|
||||
final String response;
|
||||
try (OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()))
|
||||
{
|
||||
writer.write(data.toString());
|
||||
writer.flush();
|
||||
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
response = reader.readLine();
|
||||
}
|
||||
reader.close();
|
||||
|
||||
if (response == null || response.startsWith("ERR"))
|
||||
{
|
||||
throw new IOException(response); //Throw the exception
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Encode a key/value data pair to be used in a HTTP post request. This
|
||||
* INCLUDES a & so the first key/value pair MUST be included manually,
|
||||
* e.g:</p>
|
||||
* <code>
|
||||
* StringBuffer data = new StringBuffer();
|
||||
* data.append(encode("guid")).append('=').append(encode(guid));
|
||||
* encodeDataPair(data, "version", description.getVersion());
|
||||
* </code>
|
||||
*
|
||||
* @param buffer the StringBuilder to append the data pair onto
|
||||
* @param key the key value
|
||||
* @param value the value
|
||||
*/
|
||||
private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException
|
||||
{
|
||||
buffer.append('&').append(encode(key)).append('=').append(encode(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode text as UTF-8
|
||||
*
|
||||
* @param text the text to encode
|
||||
* @return the encoded text, as UTF-8
|
||||
*/
|
||||
private static String encode(final String text) throws UnsupportedEncodingException
|
||||
{
|
||||
return URLEncoder.encode(text, "UTF-8");
|
||||
}
|
||||
}
|
131
proxy/src/main/java/net/md_5/bungee/ServerConnection.java
Normal file
131
proxy/src/main/java/net/md_5/bungee/ServerConnection.java
Normal file
@@ -0,0 +1,131 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import javax.crypto.SecretKey;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.api.Callback;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.ServerPing;
|
||||
import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.api.connection.Server;
|
||||
import net.md_5.bungee.api.event.ServerConnectedEvent;
|
||||
import net.md_5.bungee.packet.DefinedPacket;
|
||||
import net.md_5.bungee.packet.Packet1Login;
|
||||
import net.md_5.bungee.packet.Packet2Handshake;
|
||||
import net.md_5.bungee.packet.PacketCDClientStatus;
|
||||
import net.md_5.bungee.packet.PacketFAPluginMessage;
|
||||
import net.md_5.bungee.packet.PacketFCEncryptionResponse;
|
||||
import net.md_5.bungee.packet.PacketFDEncryptionRequest;
|
||||
import net.md_5.bungee.packet.PacketFFKick;
|
||||
import net.md_5.bungee.packet.PacketInputStream;
|
||||
import org.bouncycastle.crypto.io.CipherInputStream;
|
||||
import org.bouncycastle.crypto.io.CipherOutputStream;
|
||||
|
||||
/**
|
||||
* Class representing a connection from the proxy to the server; ie upstream.
|
||||
*/
|
||||
public class ServerConnection extends GenericConnection implements Server
|
||||
{
|
||||
|
||||
@Getter
|
||||
private final ServerInfo info;
|
||||
public final Packet1Login loginPacket;
|
||||
public Queue<DefinedPacket> packetQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public ServerConnection(Socket socket, ServerInfo info, PacketInputStream in, OutputStream out, Packet1Login loginPacket)
|
||||
{
|
||||
super(socket, in, out);
|
||||
this.info = info;
|
||||
this.loginPacket = loginPacket;
|
||||
}
|
||||
|
||||
public static ServerConnection connect(UserConnection user, ServerInfo info, Packet2Handshake handshake, boolean retry)
|
||||
{
|
||||
try
|
||||
{
|
||||
Socket socket = new Socket();
|
||||
socket.connect(info.getAddress(), BungeeCord.getInstance().config.getTimeout());
|
||||
BungeeCord.getInstance().setSocketOptions(socket);
|
||||
|
||||
PacketInputStream in = new PacketInputStream(socket.getInputStream());
|
||||
OutputStream out = socket.getOutputStream();
|
||||
|
||||
out.write(handshake.getPacket());
|
||||
PacketFDEncryptionRequest encryptRequest = new PacketFDEncryptionRequest(in.readPacket());
|
||||
|
||||
SecretKey myKey = EncryptionUtil.getSecret();
|
||||
PublicKey pub = EncryptionUtil.getPubkey(encryptRequest);
|
||||
|
||||
PacketFCEncryptionResponse response = new PacketFCEncryptionResponse(EncryptionUtil.getShared(myKey, pub), EncryptionUtil.encrypt(pub, encryptRequest.verifyToken));
|
||||
out.write(response.getPacket());
|
||||
|
||||
int ciphId = Util.getId(in.readPacket());
|
||||
if (ciphId != 0xFC)
|
||||
{
|
||||
throw new RuntimeException("Server did not send encryption enable");
|
||||
}
|
||||
|
||||
in = new PacketInputStream(new CipherInputStream(socket.getInputStream(), EncryptionUtil.getCipher(false, myKey)));
|
||||
out = new CipherOutputStream(out, EncryptionUtil.getCipher(true, myKey));
|
||||
|
||||
for (byte[] custom : user.loginPackets)
|
||||
{
|
||||
out.write(custom);
|
||||
}
|
||||
|
||||
out.write(new PacketCDClientStatus((byte) 0).getPacket());
|
||||
byte[] loginResponse = in.readPacket();
|
||||
if (Util.getId(loginResponse) == 0xFF)
|
||||
{
|
||||
throw new KickException("[Kicked] " + new PacketFFKick(loginResponse).message);
|
||||
}
|
||||
Packet1Login login = new Packet1Login(loginResponse);
|
||||
|
||||
ServerConnection server = new ServerConnection(socket, info, in, out, login);
|
||||
ServerConnectedEvent event = new ServerConnectedEvent(user, server);
|
||||
ProxyServer.getInstance().getPluginManager().callEvent(event);
|
||||
|
||||
out.write(BungeeCord.getInstance().registerChannels().getPacket());
|
||||
|
||||
return server;
|
||||
} catch (KickException ex)
|
||||
{
|
||||
throw ex;
|
||||
} catch (Exception ex)
|
||||
{
|
||||
ServerInfo def = ProxyServer.getInstance().getServers().get(user.getPendingConnection().getListener().getDefaultServer());
|
||||
if (retry && !info.equals(def))
|
||||
{
|
||||
user.sendMessage(ChatColor.RED + "Could not connect to target server, you have been moved to the default server");
|
||||
return connect(user, def, handshake, false);
|
||||
} else
|
||||
{
|
||||
throw new RuntimeException("Could not connect to target server " + Util.exception(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendData(String channel, byte[] data)
|
||||
{
|
||||
packetQueue.add(new PacketFAPluginMessage(channel, data));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ping(Callback<ServerPing> callback)
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getAddress()
|
||||
{
|
||||
return getInfo().getAddress();
|
||||
}
|
||||
}
|
428
proxy/src/main/java/net/md_5/bungee/UserConnection.java
Normal file
428
proxy/src/main/java/net/md_5/bungee/UserConnection.java
Normal file
@@ -0,0 +1,428 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import lombok.Getter;
|
||||
import lombok.Synchronized;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.connection.Server;
|
||||
import net.md_5.bungee.api.event.ChatEvent;
|
||||
import net.md_5.bungee.api.event.PluginMessageEvent;
|
||||
import net.md_5.bungee.api.event.ServerConnectEvent;
|
||||
import net.md_5.bungee.packet.*;
|
||||
|
||||
public class UserConnection extends GenericConnection implements ProxiedPlayer
|
||||
{
|
||||
|
||||
public final Packet2Handshake handshake;
|
||||
public Queue<DefinedPacket> packetQueue = new ConcurrentLinkedQueue<>();
|
||||
public List<byte[]> loginPackets = new ArrayList<>();
|
||||
@Getter
|
||||
private final PendingConnection pendingConnection;
|
||||
@Getter
|
||||
private ServerConnection server;
|
||||
private UpstreamBridge upBridge;
|
||||
private DownstreamBridge downBridge;
|
||||
// reconnect stuff
|
||||
private int clientEntityId;
|
||||
private int serverEntityId;
|
||||
private volatile boolean reconnecting;
|
||||
// ping stuff
|
||||
private int trackingPingId;
|
||||
private long pingTime;
|
||||
@Getter
|
||||
private int ping;
|
||||
// Permissions
|
||||
private final Collection<String> groups = new HashSet<>();
|
||||
private final Map<String, Boolean> permissions = new HashMap<>();
|
||||
private final Object permMutex = new Object();
|
||||
|
||||
public UserConnection(Socket socket, PendingConnection pendingConnection, PacketInputStream in, OutputStream out, Packet2Handshake handshake, List<byte[]> loginPackets)
|
||||
{
|
||||
super(socket, in, out);
|
||||
this.handshake = handshake;
|
||||
this.pendingConnection = pendingConnection;
|
||||
name = handshake.username;
|
||||
displayName = handshake.username;
|
||||
this.loginPackets = loginPackets;
|
||||
Collection<String> g = ProxyServer.getInstance().getConfigurationAdapter().getGroups(name);
|
||||
for (String s : g)
|
||||
{
|
||||
addGroups(s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisplayName(String name)
|
||||
{
|
||||
ProxyServer.getInstance().getTabListHandler().onDisconnect(this);
|
||||
displayName = name;
|
||||
ProxyServer.getInstance().getTabListHandler().onConnect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(ServerInfo target)
|
||||
{
|
||||
if (server == null)
|
||||
{
|
||||
// First join
|
||||
BungeeCord.getInstance().connections.put(name, this);
|
||||
ProxyServer.getInstance().getTabListHandler().onConnect(this);
|
||||
}
|
||||
|
||||
ServerConnectEvent event = new ServerConnectEvent(this, target);
|
||||
BungeeCord.getInstance().getPluginManager().callEvent(event);
|
||||
target = event.getTarget(); // Update in case the event changed target
|
||||
|
||||
ProxyServer.getInstance().getTabListHandler().onServerChange(this);
|
||||
try
|
||||
{
|
||||
reconnecting = true;
|
||||
|
||||
if (server != null)
|
||||
{
|
||||
out.write(new Packet9Respawn((byte) 1, (byte) 0, (byte) 0, (short) 256, "DEFAULT").getPacket());
|
||||
out.write(new Packet9Respawn((byte) -1, (byte) 0, (byte) 0, (short) 256, "DEFAULT").getPacket());
|
||||
}
|
||||
|
||||
ServerConnection newServer = ServerConnection.connect(this, target, handshake, true);
|
||||
if (server == null)
|
||||
{
|
||||
// Once again, first connection
|
||||
clientEntityId = newServer.loginPacket.entityId;
|
||||
serverEntityId = newServer.loginPacket.entityId;
|
||||
out.write(newServer.loginPacket.getPacket());
|
||||
out.write(BungeeCord.getInstance().registerChannels().getPacket());
|
||||
|
||||
upBridge = new UpstreamBridge();
|
||||
upBridge.start();
|
||||
} else
|
||||
{
|
||||
try
|
||||
{
|
||||
downBridge.interrupt();
|
||||
downBridge.join();
|
||||
} catch (InterruptedException ie)
|
||||
{
|
||||
}
|
||||
|
||||
server.disconnect("Quitting");
|
||||
server.getInfo().removePlayer(this);
|
||||
|
||||
Packet1Login login = newServer.loginPacket;
|
||||
serverEntityId = login.entityId;
|
||||
out.write(new Packet9Respawn(login.dimension, login.difficulty, login.gameMode, (short) 256, login.levelType).getPacket());
|
||||
}
|
||||
|
||||
// Reconnect process has finished, lets get the player moving again
|
||||
reconnecting = false;
|
||||
|
||||
// Add to new
|
||||
target.addPlayer(this);
|
||||
|
||||
// Start the bridges and move on
|
||||
server = newServer;
|
||||
downBridge = new DownstreamBridge();
|
||||
downBridge.start();
|
||||
} catch (KickException ex)
|
||||
{
|
||||
destroySelf(ex.getMessage());
|
||||
} catch (Exception ex)
|
||||
{
|
||||
ex.printStackTrace(); // TODO: Remove
|
||||
destroySelf("Could not connect to server - " + ex.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
private void destroySelf(String reason)
|
||||
{
|
||||
server.getInfo().removePlayer(this);
|
||||
ProxyServer.getInstance().getPlayers().remove(this);
|
||||
|
||||
disconnect(reason);
|
||||
if (server != null)
|
||||
{
|
||||
server.disconnect("Quitting");
|
||||
ProxyServer.getInstance().getReconnectHandler().setServer(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(String reason)
|
||||
{
|
||||
ProxyServer.getInstance().getTabListHandler().onDisconnect(this);
|
||||
super.disconnect(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message)
|
||||
{
|
||||
packetQueue.add(new Packet3Chat(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendData(String channel, byte[] data)
|
||||
{
|
||||
server.packetQueue.add(new PacketFAPluginMessage(channel, data));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getAddress()
|
||||
{
|
||||
return (InetSocketAddress) socket.getRemoteSocketAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized("permMutex")
|
||||
public Collection<String> getGroups()
|
||||
{
|
||||
return Collections.unmodifiableCollection(groups);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized("permMutex")
|
||||
public void addGroups(String... groups)
|
||||
{
|
||||
for (String group : groups)
|
||||
{
|
||||
this.groups.add(group);
|
||||
for (String permission : ProxyServer.getInstance().getConfigurationAdapter().getPermissions(group))
|
||||
{
|
||||
setPermission(permission, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized( "permMutex")
|
||||
public void removeGroups(String... groups)
|
||||
{
|
||||
for (String group : groups)
|
||||
{
|
||||
this.groups.remove(group);
|
||||
for (String permission : ProxyServer.getInstance().getConfigurationAdapter().getPermissions(group))
|
||||
{
|
||||
setPermission(permission, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized("permMutex")
|
||||
public boolean hasPermission(String permission)
|
||||
{
|
||||
Boolean val = permissions.get(permission);
|
||||
return (val == null) ? false : val;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized( "permMutex")
|
||||
public void setPermission(String permission, boolean value)
|
||||
{
|
||||
permissions.put(permission, value);
|
||||
}
|
||||
|
||||
private class UpstreamBridge extends Thread
|
||||
{
|
||||
|
||||
public UpstreamBridge()
|
||||
{
|
||||
super("Upstream Bridge - " + name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
while (!socket.isClosed())
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] packet = in.readPacket();
|
||||
boolean sendPacket = true;
|
||||
int id = Util.getId(packet);
|
||||
|
||||
switch (id)
|
||||
{
|
||||
case 0x00:
|
||||
if (trackingPingId == new Packet0KeepAlive(packet).id)
|
||||
{
|
||||
int newPing = (int) (System.currentTimeMillis() - pingTime);
|
||||
ProxyServer.getInstance().getTabListHandler().onPingChange(UserConnection.this, newPing);
|
||||
ping = newPing;
|
||||
}
|
||||
break;
|
||||
case 0x03:
|
||||
Packet3Chat chat = new Packet3Chat(packet);
|
||||
if (chat.message.startsWith("/"))
|
||||
{
|
||||
sendPacket = !ProxyServer.getInstance().getPluginManager().dispatchCommand(UserConnection.this, chat.message.substring(1));
|
||||
} else
|
||||
{
|
||||
ChatEvent chatEvent = new ChatEvent(UserConnection.this, server, chat.message);
|
||||
ProxyServer.getInstance().getPluginManager().callEvent(chatEvent);
|
||||
sendPacket = !chatEvent.isCancelled();
|
||||
}
|
||||
break;
|
||||
case 0xFA:
|
||||
// Call the onPluginMessage event
|
||||
PacketFAPluginMessage message = new PacketFAPluginMessage(packet);
|
||||
PluginMessageEvent event = new PluginMessageEvent(UserConnection.this, server, message.tag, message.data);
|
||||
ProxyServer.getInstance().getPluginManager().callEvent(event);
|
||||
|
||||
if (event.isCancelled())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
while (!server.packetQueue.isEmpty())
|
||||
{
|
||||
DefinedPacket p = server.packetQueue.poll();
|
||||
if (p != null)
|
||||
{
|
||||
server.out.write(p.getPacket());
|
||||
}
|
||||
}
|
||||
|
||||
EntityMap.rewrite(packet, clientEntityId, serverEntityId);
|
||||
if (sendPacket && !server.socket.isClosed())
|
||||
{
|
||||
server.out.write(packet);
|
||||
}
|
||||
} catch (IOException ex)
|
||||
{
|
||||
destroySelf("Reached end of stream");
|
||||
} catch (Exception ex)
|
||||
{
|
||||
destroySelf(Util.exception(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DownstreamBridge extends Thread
|
||||
{
|
||||
|
||||
public DownstreamBridge()
|
||||
{
|
||||
super("Downstream Bridge - " + name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
outer:
|
||||
while (!reconnecting)
|
||||
{
|
||||
byte[] packet = server.in.readPacket();
|
||||
int id = Util.getId(packet);
|
||||
|
||||
switch (id)
|
||||
{
|
||||
case 0x00:
|
||||
trackingPingId = new Packet0KeepAlive(packet).id;
|
||||
pingTime = System.currentTimeMillis();
|
||||
break;
|
||||
case 0x03:
|
||||
Packet3Chat chat = new Packet3Chat(packet);
|
||||
ChatEvent chatEvent = new ChatEvent(server, UserConnection.this, chat.message);
|
||||
ProxyServer.getInstance().getPluginManager().callEvent(chatEvent);
|
||||
|
||||
if (chatEvent.isCancelled())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 0xC9:
|
||||
PacketC9PlayerListItem playerList = new PacketC9PlayerListItem(packet);
|
||||
if (!ProxyServer.getInstance().getTabListHandler().onListUpdate(UserConnection.this, playerList.username, playerList.online, playerList.ping))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 0xFA:
|
||||
// Call the onPluginMessage event
|
||||
PacketFAPluginMessage message = new PacketFAPluginMessage(packet);
|
||||
DataInputStream in = new DataInputStream(new ByteArrayInputStream(message.data));
|
||||
PluginMessageEvent event = new PluginMessageEvent(server, UserConnection.this, message.tag, message.data);
|
||||
ProxyServer.getInstance().getPluginManager().callEvent(event);
|
||||
|
||||
if (event.isCancelled())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (message.tag)
|
||||
{
|
||||
case "BungeeCord::Disconnect":
|
||||
break outer;
|
||||
case "BungeeCord::Forward":
|
||||
String target = in.readUTF();
|
||||
String channel = in.readUTF();
|
||||
short len = in.readShort();
|
||||
byte[] data = new byte[len];
|
||||
in.readFully(data);
|
||||
|
||||
if (target.equals("ALL"))
|
||||
{
|
||||
for (String s : BungeeCord.getInstance().getServers().keySet())
|
||||
{
|
||||
Server server = BungeeCord.getInstance().getServer(s);
|
||||
server.sendData(channel, data);
|
||||
}
|
||||
} else
|
||||
{
|
||||
Server server = BungeeCord.getInstance().getServer(target);
|
||||
server.sendData(channel, data);
|
||||
}
|
||||
|
||||
break;
|
||||
case "BungeeCord::Connect":
|
||||
ServerInfo server = BungeeCord.getInstance().config.getServers().get(in.readUTF());
|
||||
if (server != null)
|
||||
{
|
||||
connect(server);
|
||||
break outer;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (!packetQueue.isEmpty())
|
||||
{
|
||||
DefinedPacket p = packetQueue.poll();
|
||||
if (p != null)
|
||||
{
|
||||
out.write(p.getPacket());
|
||||
}
|
||||
}
|
||||
|
||||
EntityMap.rewrite(packet, serverEntityId, clientEntityId);
|
||||
out.write(packet);
|
||||
}
|
||||
} catch (Exception ex)
|
||||
{
|
||||
destroySelf(Util.exception(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
85
proxy/src/main/java/net/md_5/bungee/Util.java
Normal file
85
proxy/src/main/java/net/md_5/bungee/Util.java
Normal file
@@ -0,0 +1,85 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
/**
|
||||
* Series of utility classes to perform various operations.
|
||||
*/
|
||||
public class Util
|
||||
{
|
||||
|
||||
private static final int DEFAULT_PORT = 25565;
|
||||
|
||||
/**
|
||||
* Method to transform human readable addresses into usable address objects.
|
||||
*
|
||||
* @param hostline in the format of 'host:port'
|
||||
* @return the constructed hostname + port.
|
||||
*/
|
||||
public static InetSocketAddress getAddr(String hostline)
|
||||
{
|
||||
String[] split = hostline.split(":");
|
||||
int port = DEFAULT_PORT;
|
||||
if (split.length > 1)
|
||||
{
|
||||
port = Integer.parseInt(split[1]);
|
||||
}
|
||||
return new InetSocketAddress(split[0], port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the first unsigned byte of the specified array. Useful
|
||||
* for getting the id of a packet array .
|
||||
*
|
||||
* @param b the array to read from
|
||||
* @return the unsigned value of the first byte
|
||||
*/
|
||||
public static int getId(byte[] b)
|
||||
{
|
||||
return b[0] & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a config path by prefix upper case letters with '_' and
|
||||
* turning them to lowercase.
|
||||
*
|
||||
* @param s the string to normalize
|
||||
* @return the normalized path
|
||||
*/
|
||||
public static String normalize(String s)
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (char c : s.toCharArray())
|
||||
{
|
||||
if (Character.isUpperCase(c))
|
||||
{
|
||||
result.append("_");
|
||||
}
|
||||
result.append(Character.toLowerCase(c));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an integer as a hex value.
|
||||
*
|
||||
* @param i the integer to format
|
||||
* @return the hex representation of the integer
|
||||
*/
|
||||
public static String hex(int i)
|
||||
{
|
||||
return String.format("0x%02X", i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a pretty one line version of a {@link Throwable}. Useful for
|
||||
* debugging.
|
||||
*
|
||||
* @param t the {@link Throwable} to format.
|
||||
* @return a string representing information about the {@link Throwable}
|
||||
*/
|
||||
public static String exception(Throwable t)
|
||||
{
|
||||
return t.getClass().getSimpleName() + " : " + t.getMessage() + " @ " + t.getStackTrace()[0].getClassName() + ":" + t.getStackTrace()[0].getLineNumber();
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
package net.md_5.bungee;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Level;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.ReconnectHandler;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
public class YamlReconnectHandler implements ReconnectHandler
|
||||
{
|
||||
|
||||
private final Yaml yaml = new Yaml();
|
||||
private final File file = new File("locations.yml");
|
||||
/*========================================================================*/
|
||||
private Map<String, String> data;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public YamlReconnectHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
file.createNewFile();
|
||||
try (FileReader rd = new FileReader(file))
|
||||
{
|
||||
data = yaml.loadAs(rd, Map.class);
|
||||
}
|
||||
} catch (IOException ex)
|
||||
{
|
||||
ProxyServer.getInstance().getLogger().log(Level.WARNING, "Could not load reconnect locations", ex);
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
data = new ConcurrentHashMap<>();
|
||||
} else
|
||||
{
|
||||
data = new ConcurrentHashMap<>(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServer(ProxiedPlayer player)
|
||||
{
|
||||
ListenerInfo listener = player.getPendingConnection().getListener();
|
||||
if (listener.isForceDefault())
|
||||
{
|
||||
return listener.getDefaultServer();
|
||||
}
|
||||
String forced = listener.getForcedHosts().get(player.getPendingConnection().getVirtualHost().getHostName());
|
||||
String server = (forced == null) ? data.get(key(player)) : forced;
|
||||
return (server != null) ? server : listener.getDefaultServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setServer(ProxiedPlayer player)
|
||||
{
|
||||
data.put(key(player), player.getServer().getInfo().getName());
|
||||
}
|
||||
|
||||
private String key(ProxiedPlayer player)
|
||||
{
|
||||
InetSocketAddress host = player.getPendingConnection().getVirtualHost();
|
||||
return player.getName() + ";" + host.getHostString() + ":" + host.getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
try (FileWriter wr = new FileWriter(file))
|
||||
{
|
||||
yaml.dump(data, wr);
|
||||
} catch (IOException ex)
|
||||
{
|
||||
ProxyServer.getInstance().getLogger().log(Level.WARNING, "Could not save reconnect locations", ex);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package net.md_5.bungee.command;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
|
||||
public class CommandAlert extends Command
|
||||
{
|
||||
|
||||
public CommandAlert()
|
||||
{
|
||||
super("alert", "bungee.command.alert");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args)
|
||||
{
|
||||
if (args.length == 0)
|
||||
{
|
||||
sender.sendMessage(ChatColor.RED + "You must supply a message.");
|
||||
} else
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (!args[0].startsWith("&h"))
|
||||
{
|
||||
builder.append(ChatColor.DARK_PURPLE);
|
||||
builder.append("[Alert] ");
|
||||
}
|
||||
|
||||
for (String s : args)
|
||||
{
|
||||
s = s.substring(1, s.length());
|
||||
builder.append(ChatColor.translateAlternateColorCodes('&', s));
|
||||
builder.append(" ");
|
||||
}
|
||||
|
||||
String message = builder.substring(0, builder.length() - 1);
|
||||
for (ProxiedPlayer player : ProxyServer.getInstance().getPlayers())
|
||||
{
|
||||
player.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package net.md_5.bungee.command;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
|
||||
public class CommandBungee extends Command
|
||||
{
|
||||
|
||||
public CommandBungee()
|
||||
{
|
||||
super("bungee");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args)
|
||||
{
|
||||
sender.sendMessage(ChatColor.BLUE + "This server is running BungeeCord version " + ProxyServer.getInstance().getVersion() + " by md_5");
|
||||
}
|
||||
}
|
23
proxy/src/main/java/net/md_5/bungee/command/CommandEnd.java
Normal file
23
proxy/src/main/java/net/md_5/bungee/command/CommandEnd.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package net.md_5.bungee.command;
|
||||
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
|
||||
/**
|
||||
* Command to terminate the proxy instance. May only be used by the console.
|
||||
*/
|
||||
public class CommandEnd extends Command
|
||||
{
|
||||
|
||||
public CommandEnd()
|
||||
{
|
||||
super("end", "bungeecord.command.end");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args)
|
||||
{
|
||||
BungeeCord.getInstance().stop();
|
||||
}
|
||||
}
|
34
proxy/src/main/java/net/md_5/bungee/command/CommandIP.java
Normal file
34
proxy/src/main/java/net/md_5/bungee/command/CommandIP.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package net.md_5.bungee.command;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
|
||||
public class CommandIP extends Command
|
||||
{
|
||||
|
||||
public CommandIP()
|
||||
{
|
||||
super("ip", "bungeecord.command.ip");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args)
|
||||
{
|
||||
if (args.length < 1)
|
||||
{
|
||||
sender.sendMessage(ChatColor.RED + "Please follow this command by a user name");
|
||||
return;
|
||||
}
|
||||
ProxiedPlayer user = ProxyServer.getInstance().getPlayer(args[0]);
|
||||
if (user == null)
|
||||
{
|
||||
sender.sendMessage(ChatColor.RED + "That user is not online");
|
||||
} else
|
||||
{
|
||||
sender.sendMessage(ChatColor.BLUE + "IP of " + args[0] + " is " + user.getAddress());
|
||||
}
|
||||
}
|
||||
}
|
43
proxy/src/main/java/net/md_5/bungee/command/CommandList.java
Normal file
43
proxy/src/main/java/net/md_5/bungee/command/CommandList.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package net.md_5.bungee.command;
|
||||
|
||||
import java.util.Collection;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
|
||||
/**
|
||||
* Command to list all players connected to the proxy.
|
||||
*/
|
||||
public class CommandList extends Command
|
||||
{
|
||||
|
||||
public CommandList()
|
||||
{
|
||||
super("list", "bungeecord.command.list");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args)
|
||||
{
|
||||
StringBuilder users = new StringBuilder();
|
||||
Collection<ProxiedPlayer> connections = ProxyServer.getInstance().getPlayers();
|
||||
|
||||
if (connections.isEmpty())
|
||||
{
|
||||
sender.sendMessage(ChatColor.BLUE + "Currently no players online.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (ProxiedPlayer player : connections)
|
||||
{
|
||||
users.append(player.getDisplayName());
|
||||
users.append(", ");
|
||||
users.append(ChatColor.RESET);
|
||||
}
|
||||
|
||||
users.setLength(users.length() - 2);
|
||||
sender.sendMessage(ChatColor.BLUE + "Currently online across all servers (" + connections.size() + "): " + ChatColor.RESET + users);
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package net.md_5.bungee.command;
|
||||
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
|
||||
public class CommandReload extends Command
|
||||
{
|
||||
|
||||
public CommandReload()
|
||||
{
|
||||
super("greload", "bungeecord.command.reload");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args)
|
||||
{
|
||||
BungeeCord.getInstance().config.load();
|
||||
sender.sendMessage(ChatColor.GREEN + "Reloaded config, please restart if you have any issues");
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
package net.md_5.bungee.command;
|
||||
|
||||
import java.util.Map;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
|
||||
/**
|
||||
* Command to list and switch a player between available servers.
|
||||
*/
|
||||
public class CommandServer extends Command
|
||||
{
|
||||
|
||||
public CommandServer()
|
||||
{
|
||||
super("server", "bungeecord.command.server");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args)
|
||||
{
|
||||
if (!(sender instanceof ProxiedPlayer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
ProxiedPlayer player = (ProxiedPlayer) sender;
|
||||
Map<String, ServerInfo> servers = BungeeCord.getInstance().config.getServers();
|
||||
if (args.length == 0)
|
||||
{
|
||||
StringBuilder serverList = new StringBuilder();
|
||||
for (String server : servers.keySet())
|
||||
{
|
||||
serverList.append(server);
|
||||
serverList.append(", ");
|
||||
}
|
||||
serverList.setLength(serverList.length() - 2);
|
||||
player.sendMessage(ChatColor.GOLD + "You may connect to the following servers at this time: " + serverList.toString());
|
||||
} else
|
||||
{
|
||||
ServerInfo server = servers.get(args[0]);
|
||||
if (server == null)
|
||||
{
|
||||
player.sendMessage(ChatColor.RED + "The specified server does not exist");
|
||||
} else if (server.equals(player.getServer().getInfo()))
|
||||
{
|
||||
player.sendMessage(ChatColor.RED + "You are already on this server.");
|
||||
} else
|
||||
{
|
||||
player.connect(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package net.md_5.bungee.command;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
|
||||
/**
|
||||
* Command sender representing the proxy console.
|
||||
*/
|
||||
public class ConsoleCommandSender implements CommandSender
|
||||
{
|
||||
|
||||
@Getter
|
||||
private static final ConsoleCommandSender instance = new ConsoleCommandSender();
|
||||
|
||||
private ConsoleCommandSender()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message)
|
||||
{
|
||||
System.out.println(ChatColor.stripColor(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return "CONSOLE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getGroups()
|
||||
{
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGroups(String... groups)
|
||||
{
|
||||
throw new UnsupportedOperationException("Console may not have groups");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeGroups(String... groups)
|
||||
{
|
||||
throw new UnsupportedOperationException("Console may not have groups");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(String permission)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPermission(String permission, boolean value)
|
||||
{
|
||||
throw new UnsupportedOperationException("Console has all permissions");
|
||||
}
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
package net.md_5.bungee.config;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ConfigurationAdapter;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.tablist.GlobalPingTabList;
|
||||
import net.md_5.bungee.tablist.GlobalTabList;
|
||||
import net.md_5.bungee.tablist.ServerUniqueTabList;
|
||||
|
||||
/**
|
||||
* Core configuration for the proxy.
|
||||
*/
|
||||
@Getter
|
||||
public class Configuration
|
||||
{
|
||||
|
||||
/**
|
||||
* The default tab list options available for picking.
|
||||
*/
|
||||
private enum DefaultTabList
|
||||
{
|
||||
|
||||
GLOBAL, GLOBAL_PING, SERVER;
|
||||
}
|
||||
/**
|
||||
* Time before users are disconnected due to no network activity.
|
||||
*/
|
||||
private int timeout = 30000;
|
||||
/**
|
||||
* UUID used for metrics.
|
||||
*/
|
||||
private String uuid = UUID.randomUUID().toString();
|
||||
/**
|
||||
* Set of all listeners.
|
||||
*/
|
||||
private Collection<ListenerInfo> listeners;
|
||||
/**
|
||||
* Set of all servers.
|
||||
*/
|
||||
private Map<String, ServerInfo> servers;
|
||||
|
||||
public void load()
|
||||
{
|
||||
ConfigurationAdapter adapter = ProxyServer.getInstance().getConfigurationAdapter();
|
||||
|
||||
timeout = adapter.getInt("timeout", timeout);
|
||||
uuid = adapter.getString("stats", uuid);
|
||||
|
||||
DefaultTabList tab = DefaultTabList.valueOf(adapter.getString("tab_list", "GLOBAL_PING"));
|
||||
if (tab == null)
|
||||
{
|
||||
tab = DefaultTabList.GLOBAL_PING;
|
||||
}
|
||||
switch (tab)
|
||||
{
|
||||
case GLOBAL:
|
||||
ProxyServer.getInstance().setTabListHandler(new GlobalTabList());
|
||||
break;
|
||||
case GLOBAL_PING:
|
||||
ProxyServer.getInstance().setTabListHandler(new GlobalPingTabList());
|
||||
break;
|
||||
case SERVER:
|
||||
ProxyServer.getInstance().setTabListHandler(new ServerUniqueTabList());
|
||||
break;
|
||||
}
|
||||
|
||||
listeners = adapter.getListeners();
|
||||
Preconditions.checkArgument(listeners != null && !listeners.isEmpty(), "No listeners defined.");
|
||||
|
||||
servers = adapter.getServers();
|
||||
Preconditions.checkArgument(servers != null && !servers.isEmpty(), "No servers defined");
|
||||
|
||||
for (ListenerInfo listener : listeners)
|
||||
{
|
||||
Preconditions.checkArgument(servers.containsKey(listener.getDefaultServer()));
|
||||
}
|
||||
}
|
||||
}
|
205
proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java
Normal file
205
proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java
Normal file
@@ -0,0 +1,205 @@
|
||||
package net.md_5.bungee.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import net.md_5.bungee.Util;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ConfigurationAdapter;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.config.ServerInfo;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
public class YamlConfig implements ConfigurationAdapter
|
||||
{
|
||||
|
||||
private boolean loaded;
|
||||
private Yaml yaml;
|
||||
private Map config;
|
||||
private final File file = new File("config.yml");
|
||||
|
||||
public void load()
|
||||
{
|
||||
try
|
||||
{
|
||||
file.createNewFile();
|
||||
DumperOptions options = new DumperOptions();
|
||||
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
yaml = new Yaml(options);
|
||||
|
||||
try (InputStream is = new FileInputStream(file))
|
||||
{
|
||||
config = (Map) yaml.load(is);
|
||||
}
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
config = new HashMap();
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
} catch (IOException ex)
|
||||
{
|
||||
throw new RuntimeException("Could not load configuration!", ex);
|
||||
}
|
||||
|
||||
Map<String, Object> permissions = get("permissions", new HashMap<String, Object>());
|
||||
if (permissions.isEmpty())
|
||||
{
|
||||
permissions.put("default", Arrays.asList(new String[]
|
||||
{
|
||||
"bungeecord.command.server", "bungeecord.command.list"
|
||||
}));
|
||||
permissions.put("admin", Arrays.asList(new String[]
|
||||
{
|
||||
"bungeecord.command.alert", "bungeecord.command.end", "bungeecord.command.ip", "bungeecord.command.reload"
|
||||
}));
|
||||
}
|
||||
|
||||
Map<String, Object> groups = get("groups", new HashMap<String, Object>());
|
||||
if (groups.isEmpty())
|
||||
{
|
||||
groups.put("md_5", Collections.singletonList("admin"));
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T get(String path, T def)
|
||||
{
|
||||
if (!loaded)
|
||||
{
|
||||
load();
|
||||
}
|
||||
return get(path, def, config);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T get(String path, T def, Map submap)
|
||||
{
|
||||
if (!loaded)
|
||||
{
|
||||
load();
|
||||
}
|
||||
|
||||
int index = path.indexOf('.');
|
||||
if (index == -1)
|
||||
{
|
||||
Object val = submap.get(path);
|
||||
if (val == null)
|
||||
{
|
||||
val = def;
|
||||
submap.put(path, def);
|
||||
save();
|
||||
}
|
||||
return (T) val;
|
||||
} else
|
||||
{
|
||||
String first = path.substring(0, index);
|
||||
String second = path.substring(index + 1, path.length());
|
||||
Map sub = (Map) submap.get(first);
|
||||
return (sub != null) ? get(second, def, sub) : def;
|
||||
}
|
||||
}
|
||||
|
||||
private void save()
|
||||
{
|
||||
try
|
||||
{
|
||||
try (FileWriter wr = new FileWriter(file))
|
||||
{
|
||||
yaml.dump(config, wr);
|
||||
}
|
||||
} catch (IOException ex)
|
||||
{
|
||||
ProxyServer.getInstance().getLogger().log(Level.WARNING, "Could not save config", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String path, int def)
|
||||
{
|
||||
return get(path, def);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String path, String def)
|
||||
{
|
||||
return get(path, def);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, ServerInfo> getServers()
|
||||
{
|
||||
Map<String, Map<String, Object>> base = get("servers", (Map) Collections.singletonMap("lobby", new HashMap<>()));
|
||||
Map<String, ServerInfo> ret = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, Map<String, Object>> entry : base.entrySet())
|
||||
{
|
||||
Map<String, Object> val = entry.getValue();
|
||||
String name = entry.getKey();
|
||||
String addr = get("address", "localhost:25565", val);
|
||||
InetSocketAddress address = Util.getAddr(addr);
|
||||
ServerInfo info = new ServerInfo(name, address);
|
||||
ret.put(name, info);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Collection<ListenerInfo> getListeners()
|
||||
{
|
||||
Collection<Map<String, Object>> base = get("listeners", (Collection) Arrays.asList(new Map[]
|
||||
{
|
||||
new HashMap()
|
||||
}));
|
||||
Map<String, String> forcedDef = new HashMap<>();
|
||||
forcedDef.put("pvp.md-5.net", "pvp");
|
||||
|
||||
Collection<ListenerInfo> ret = new HashSet<>();
|
||||
|
||||
for (Map<String, Object> val : base)
|
||||
{
|
||||
String motd = get("motd", "Another Bungee server", val);
|
||||
int maxPlayers = get("max_players", 1, val);
|
||||
String defaultServer = get("default_server", "lobby", val);
|
||||
boolean forceDefault = get("force_default_server", false, val);
|
||||
String host = get("host", "0.0.0.0:25577", val);
|
||||
InetSocketAddress address = Util.getAddr(host);
|
||||
Map<String, String> forced = get("forced_hosts", forcedDef, val);
|
||||
ListenerInfo info = new ListenerInfo(address, motd, maxPlayers, defaultServer, forceDefault, forced);
|
||||
ret.add(info);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Collection<String> getGroups(String player)
|
||||
{
|
||||
Collection<String> groups = get("groups." + player, Collections.EMPTY_LIST);
|
||||
Collection<String> ret = new HashSet<>(groups);
|
||||
ret.add("default");
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Collection<String> getPermissions(String group)
|
||||
{
|
||||
return get("permissions." + group, Collections.EMPTY_SET);
|
||||
}
|
||||
}
|
109
proxy/src/main/java/net/md_5/bungee/packet/DefinedPacket.java
Normal file
109
proxy/src/main/java/net/md_5/bungee/packet/DefinedPacket.java
Normal file
@@ -0,0 +1,109 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import com.google.common.io.ByteArrayDataInput;
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutput;
|
||||
import lombok.Delegate;
|
||||
import net.md_5.bungee.Util;
|
||||
|
||||
/**
|
||||
* This class represents a packet which has been given a special definition. All
|
||||
* subclasses can read and write to the backing byte array which can be
|
||||
* retrieved via the {@link #getPacket()} method.
|
||||
*/
|
||||
public abstract class DefinedPacket implements DataInput, DataOutput
|
||||
{
|
||||
|
||||
private interface Overriden
|
||||
{
|
||||
|
||||
void readUTF();
|
||||
|
||||
void writeUTF(String s);
|
||||
}
|
||||
@Delegate(excludes = Overriden.class)
|
||||
private ByteArrayDataInput in;
|
||||
@Delegate(excludes = Overriden.class)
|
||||
private ByteArrayDataOutput out;
|
||||
/**
|
||||
* Packet id.
|
||||
*/
|
||||
public final int id;
|
||||
/**
|
||||
* Already constructed packet.
|
||||
*/
|
||||
private byte[] packet;
|
||||
|
||||
public DefinedPacket(int id, byte[] buf)
|
||||
{
|
||||
in = ByteStreams.newDataInput(buf);
|
||||
if (readUnsignedByte() != id)
|
||||
{
|
||||
throw new IllegalArgumentException("Wasn't expecting packet id " + Util.hex(id));
|
||||
}
|
||||
this.id = id;
|
||||
packet = buf;
|
||||
}
|
||||
|
||||
public DefinedPacket(int id)
|
||||
{
|
||||
out = ByteStreams.newDataOutput();
|
||||
this.id = id;
|
||||
writeByte(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bytes that make up this packet.
|
||||
*
|
||||
* @return the bytes which make up this packet, either the original byte
|
||||
* array or the newly written one.
|
||||
*/
|
||||
public byte[] getPacket()
|
||||
{
|
||||
return packet == null ? out.toByteArray() : packet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeUTF(String s)
|
||||
{
|
||||
writeShort(s.length());
|
||||
writeChars(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readUTF()
|
||||
{
|
||||
short len = readShort();
|
||||
char[] chars = new char[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
chars[i] = this.readChar();
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
public void writeArray(byte[] b)
|
||||
{
|
||||
writeShort(b.length);
|
||||
write(b);
|
||||
}
|
||||
|
||||
public byte[] readArray()
|
||||
{
|
||||
short len = readShort();
|
||||
byte[] ret = new byte[len];
|
||||
readFully(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract boolean equals(Object obj);
|
||||
|
||||
@Override
|
||||
public abstract int hashCode();
|
||||
|
||||
@Override
|
||||
public abstract String toString();
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class Packet0KeepAlive extends DefinedPacket
|
||||
{
|
||||
|
||||
public int id;
|
||||
|
||||
public Packet0KeepAlive(byte[] buffer)
|
||||
{
|
||||
super(0x00, buffer);
|
||||
id = readInt();
|
||||
}
|
||||
}
|
42
proxy/src/main/java/net/md_5/bungee/packet/Packet1Login.java
Normal file
42
proxy/src/main/java/net/md_5/bungee/packet/Packet1Login.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class Packet1Login extends DefinedPacket
|
||||
{
|
||||
|
||||
public int entityId;
|
||||
public String levelType;
|
||||
public byte gameMode;
|
||||
public byte dimension;
|
||||
public byte difficulty;
|
||||
public byte unused;
|
||||
public byte maxPlayers;
|
||||
|
||||
public Packet1Login(int entityId, String levelType, byte gameMode, byte dimension, byte difficulty, byte unused, byte maxPlayers)
|
||||
{
|
||||
super(0x01);
|
||||
writeInt(entityId);
|
||||
writeUTF(levelType);
|
||||
writeByte(gameMode);
|
||||
writeByte(dimension);
|
||||
writeByte(difficulty);
|
||||
writeByte(unused);
|
||||
writeByte(maxPlayers);
|
||||
}
|
||||
|
||||
public Packet1Login(byte[] buf)
|
||||
{
|
||||
super(0x01, buf);
|
||||
this.entityId = readInt();
|
||||
this.levelType = readUTF();
|
||||
this.gameMode = readByte();
|
||||
this.dimension = readByte();
|
||||
this.difficulty = readByte();
|
||||
this.unused = readByte();
|
||||
this.maxPlayers = readByte();
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class Packet2Handshake extends DefinedPacket
|
||||
{
|
||||
|
||||
public byte procolVersion;
|
||||
public String username;
|
||||
public String host;
|
||||
public int port;
|
||||
|
||||
public Packet2Handshake(byte protocolVersion, String username, String host, int port)
|
||||
{
|
||||
super(0x02);
|
||||
writeByte(protocolVersion);
|
||||
writeUTF(username);
|
||||
writeUTF(host);
|
||||
writeInt(port);
|
||||
}
|
||||
|
||||
public Packet2Handshake(byte[] buf)
|
||||
{
|
||||
super(0x02, buf);
|
||||
this.procolVersion = readByte();
|
||||
this.username = readUTF();
|
||||
this.host = readUTF();
|
||||
this.port = readInt();
|
||||
}
|
||||
}
|
24
proxy/src/main/java/net/md_5/bungee/packet/Packet3Chat.java
Normal file
24
proxy/src/main/java/net/md_5/bungee/packet/Packet3Chat.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class Packet3Chat extends DefinedPacket
|
||||
{
|
||||
|
||||
public String message;
|
||||
|
||||
public Packet3Chat(String message)
|
||||
{
|
||||
super(0x03);
|
||||
writeUTF(message);
|
||||
}
|
||||
|
||||
public Packet3Chat(byte[] buf)
|
||||
{
|
||||
super(0x03, buf);
|
||||
this.message = readUTF();
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class Packet9Respawn extends DefinedPacket
|
||||
{
|
||||
|
||||
public int dimension;
|
||||
public byte difficulty;
|
||||
public byte gameMode;
|
||||
public short worldHeight;
|
||||
public String levelType;
|
||||
|
||||
public Packet9Respawn(int dimension, byte difficulty, byte gameMode, short worldHeight, String levelType)
|
||||
{
|
||||
super(0x09);
|
||||
writeInt(dimension);
|
||||
writeByte(difficulty);
|
||||
writeByte(gameMode);
|
||||
writeShort(worldHeight);
|
||||
writeUTF(levelType);
|
||||
}
|
||||
|
||||
public Packet9Respawn(byte[] buf)
|
||||
{
|
||||
super(0x09, buf);
|
||||
this.dimension = readInt();
|
||||
this.difficulty = readByte();
|
||||
this.gameMode = readByte();
|
||||
this.worldHeight = readShort();
|
||||
this.levelType = readUTF();
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class PacketC9PlayerListItem extends DefinedPacket
|
||||
{
|
||||
|
||||
public String username;
|
||||
public boolean online;
|
||||
public int ping;
|
||||
|
||||
public PacketC9PlayerListItem(byte[] packet)
|
||||
{
|
||||
super(0xC9, packet);
|
||||
username = readUTF();
|
||||
online = readBoolean();
|
||||
ping = readShort();
|
||||
}
|
||||
|
||||
public PacketC9PlayerListItem(String username, boolean online, int ping)
|
||||
{
|
||||
super(0xC9);
|
||||
writeUTF(username);
|
||||
writeBoolean(online);
|
||||
writeShort(ping);
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class PacketCDClientStatus extends DefinedPacket
|
||||
{
|
||||
|
||||
/**
|
||||
* Sent from the client to the server upon respawn,
|
||||
*
|
||||
* @param payload 0 if initial spawn, 1 if respawn after death.
|
||||
*/
|
||||
public PacketCDClientStatus(byte payload)
|
||||
{
|
||||
super(0xCD);
|
||||
writeByte(payload);
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class PacketFAPluginMessage extends DefinedPacket
|
||||
{
|
||||
|
||||
public String tag;
|
||||
public byte[] data;
|
||||
|
||||
public PacketFAPluginMessage(String tag, byte[] data)
|
||||
{
|
||||
super(0xFA);
|
||||
writeUTF(tag);
|
||||
writeArray(data);
|
||||
this.tag = tag;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public PacketFAPluginMessage(byte[] buf)
|
||||
{
|
||||
super(0xFA, buf);
|
||||
this.tag = readUTF();
|
||||
this.data = readArray();
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class PacketFCEncryptionResponse extends DefinedPacket
|
||||
{
|
||||
|
||||
public byte[] sharedSecret;
|
||||
public byte[] verifyToken;
|
||||
|
||||
public PacketFCEncryptionResponse()
|
||||
{
|
||||
super(0xFC);
|
||||
writeArray(new byte[0]);
|
||||
writeArray(new byte[0]);
|
||||
}
|
||||
|
||||
public PacketFCEncryptionResponse(byte[] sharedSecret, byte[] verifyToken)
|
||||
{
|
||||
super(0xFC);
|
||||
writeArray(sharedSecret);
|
||||
writeArray(verifyToken);
|
||||
}
|
||||
|
||||
public PacketFCEncryptionResponse(byte[] buf)
|
||||
{
|
||||
super(0xFC, buf);
|
||||
this.sharedSecret = readArray();
|
||||
this.verifyToken = readArray();
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class PacketFDEncryptionRequest extends DefinedPacket
|
||||
{
|
||||
|
||||
public String serverId;
|
||||
public byte[] publicKey;
|
||||
public byte[] verifyToken;
|
||||
|
||||
public PacketFDEncryptionRequest(String serverId, byte[] publicKey, byte[] verifyToken)
|
||||
{
|
||||
super(0xFD);
|
||||
writeUTF(serverId);
|
||||
writeArray(publicKey);
|
||||
writeArray(verifyToken);
|
||||
this.serverId = serverId;
|
||||
this.publicKey = publicKey;
|
||||
this.verifyToken = verifyToken;
|
||||
}
|
||||
|
||||
public PacketFDEncryptionRequest(byte[] buf)
|
||||
{
|
||||
super(0xFD, buf);
|
||||
serverId = readUTF();
|
||||
publicKey = readArray();
|
||||
verifyToken = readArray();
|
||||
}
|
||||
}
|
24
proxy/src/main/java/net/md_5/bungee/packet/PacketFFKick.java
Normal file
24
proxy/src/main/java/net/md_5/bungee/packet/PacketFFKick.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class PacketFFKick extends DefinedPacket
|
||||
{
|
||||
|
||||
public String message;
|
||||
|
||||
public PacketFFKick(String message)
|
||||
{
|
||||
super(0xFF);
|
||||
writeUTF(message);
|
||||
}
|
||||
|
||||
public PacketFFKick(byte[] buf)
|
||||
{
|
||||
super(0xFF, buf);
|
||||
this.message = readUTF();
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package net.md_5.bungee.packet;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import net.md_5.mendax.datainput.DataInputPacketReader;
|
||||
|
||||
/**
|
||||
* A specialized input stream to parse packets using the Mojang packet
|
||||
* definitions and then return them as a byte array.
|
||||
*/
|
||||
public class PacketInputStream
|
||||
{
|
||||
|
||||
private final DataInputStream dataInput;
|
||||
private final TrackingInputStream tracker;
|
||||
|
||||
public PacketInputStream(InputStream in)
|
||||
{
|
||||
tracker = new TrackingInputStream(in);
|
||||
dataInput = new DataInputStream(tracker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an entire packet from the stream and return it as a byte array.
|
||||
*
|
||||
* @return the read packet
|
||||
* @throws IOException when the underlying input stream throws an exception
|
||||
*/
|
||||
public byte[] readPacket() throws IOException
|
||||
{
|
||||
tracker.out.reset();
|
||||
DataInputPacketReader.readPacket(dataInput);
|
||||
return tracker.out.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Input stream which will wrap another stream and copy all bytes read to a
|
||||
* {@link ByteArrayOutputStream}.
|
||||
*/
|
||||
private class TrackingInputStream extends InputStream
|
||||
{
|
||||
|
||||
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
private final InputStream wrapped;
|
||||
|
||||
public TrackingInputStream(InputStream wrapped)
|
||||
{
|
||||
this.wrapped = wrapped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException
|
||||
{
|
||||
int ret = wrapped.read();
|
||||
out.write(ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package net.md_5.bungee.tablist;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.packet.PacketC9PlayerListItem;
|
||||
|
||||
public class GlobalPingTabList extends GlobalTabList
|
||||
{
|
||||
|
||||
private static final int PING_THRESHOLD = 20;
|
||||
private final Map<ProxiedPlayer, Integer> lastPings = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onDisconnect(ProxiedPlayer player)
|
||||
{
|
||||
lastPings.remove(player);
|
||||
super.onDisconnect(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPingChange(ProxiedPlayer player, int ping)
|
||||
{
|
||||
Integer lastPing = lastPings.get(player);
|
||||
if (lastPing == null || (ping - PING_THRESHOLD > lastPing && ping + PING_THRESHOLD < lastPing))
|
||||
{
|
||||
BungeeCord.getInstance().broadcast(new PacketC9PlayerListItem(player.getDisplayName(), true, ping));
|
||||
lastPings.put(player, ping);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
package net.md_5.bungee.tablist;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import lombok.Synchronized;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.UserConnection;
|
||||
import net.md_5.bungee.api.TabListHandler;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.packet.PacketC9PlayerListItem;
|
||||
|
||||
public class GlobalTabList implements TabListHandler
|
||||
{
|
||||
|
||||
private final Set<ProxiedPlayer> sentPings = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void onConnect(ProxiedPlayer player)
|
||||
{
|
||||
for (UserConnection c : BungeeCord.getInstance().connections.values())
|
||||
{
|
||||
c.packetQueue.add(new PacketC9PlayerListItem(c.displayName, true, c.getPing()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized(value = "sentPings")
|
||||
public void onPingChange(ProxiedPlayer player, int ping)
|
||||
{
|
||||
if (!sentPings.contains(player))
|
||||
{
|
||||
BungeeCord.getInstance().broadcast(new PacketC9PlayerListItem(player.getDisplayName(), true, player.getPing()));
|
||||
sentPings.add(player);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Synchronized(value = "sentPings")
|
||||
public void onDisconnect(ProxiedPlayer player)
|
||||
{
|
||||
BungeeCord.getInstance().broadcast(new PacketC9PlayerListItem(player.getDisplayName(), false, 9999));
|
||||
sentPings.remove(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerChange(ProxiedPlayer player)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListUpdate(ProxiedPlayer player, String name, boolean online, int ping)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
package net.md_5.bungee.tablist;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.md_5.bungee.UserConnection;
|
||||
import net.md_5.bungee.api.TabListHandler;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.packet.PacketC9PlayerListItem;
|
||||
|
||||
public class ServerUniqueTabList implements TabListHandler
|
||||
{
|
||||
|
||||
private final Map<ProxiedPlayer, Set<String>> sentUsernames = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onConnect(ProxiedPlayer player)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPingChange(ProxiedPlayer player, int ping)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(ProxiedPlayer player)
|
||||
{
|
||||
sentUsernames.remove(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerChange(ProxiedPlayer player)
|
||||
{
|
||||
Set<String> usernames = sentUsernames.get(player);
|
||||
if (usernames != null)
|
||||
{
|
||||
synchronized (usernames)
|
||||
{
|
||||
for (String username : usernames)
|
||||
{
|
||||
((UserConnection) player).packetQueue.add(new PacketC9PlayerListItem(username, false, 9999));
|
||||
}
|
||||
usernames.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListUpdate(ProxiedPlayer player, String name, boolean online, int ping)
|
||||
{
|
||||
Set<String> usernames = sentUsernames.get(player);
|
||||
if (usernames == null)
|
||||
{
|
||||
usernames = new HashSet<>();
|
||||
sentUsernames.put(player, usernames);
|
||||
}
|
||||
|
||||
synchronized (usernames)
|
||||
{
|
||||
if (online)
|
||||
{
|
||||
usernames.add(name);
|
||||
} else
|
||||
{
|
||||
usernames.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user