Merge NIO into master. I would not recommend this on a production server at all. Its 1.5 anyway.

This commit is contained in:
md_5 2013-03-11 13:29:17 +11:00
commit a3e1493ce1
63 changed files with 1464 additions and 1437 deletions

View File

@ -16,4 +16,14 @@ public interface Connection
* @return the remote address * @return the remote address
*/ */
public InetSocketAddress getAddress(); public InetSocketAddress getAddress();
/**
* Disconnects this end of the connection for the specified reason. If this
* is an {@link ProxiedPlayer} the respective server connection will be
* closed too.
*
* @param reason the reason shown to the player / sent to the server on
* disconnect
*/
public void disconnect(String reason);
} }

View File

@ -30,14 +30,6 @@ public interface PendingConnection extends Connection
*/ */
public InetSocketAddress getVirtualHost(); public InetSocketAddress getVirtualHost();
/**
* Completely kick this user from the proxy and all of its child
* connections.
*
* @param reason the disconnect reason displayed to the player
*/
public void disconnect(String reason);
/** /**
* Get the listener that accepted this connection. * Get the listener that accepted this connection.
* *

View File

@ -48,13 +48,6 @@ public interface ProxiedPlayer extends Connection, CommandSender
*/ */
public int getPing(); public int getPing();
/**
* Disconnect (remove) this player from the proxy with the specified reason.
*
* @param reason the reason displayed to the player
*/
public void disconnect(String reason);
/** /**
* Send a plugin message to this player. * Send a plugin message to this player.
* *

View File

@ -1,7 +1,5 @@
package net.md_5.bungee.api.connection; package net.md_5.bungee.api.connection;
import net.md_5.bungee.api.Callback;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.config.ServerInfo;
/** /**
@ -24,14 +22,4 @@ public interface Server extends Connection
* @param data the data to send * @param data the data to send
*/ */
public abstract void sendData(String channel, byte[] data); public abstract void sendData(String channel, byte[] data);
/**
* Asynchronously gets the current player count on this server.
*
* @param callback the callback to call when the count has been retrieved.
* @deprecated use the corresponding method in {@link ServerInfo} for
* clarity
*/
@Deprecated
public abstract void ping(Callback<ServerPing> callback);
} }

View File

@ -92,7 +92,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version> <version>2.5.1</version>
<configuration> <configuration>
<source>1.7</source> <source>1.7</source>
<target>1.7</target> <target>1.7</target>

View File

@ -17,4 +17,14 @@
<name>BungeeCord-Protocol</name> <name>BungeeCord-Protocol</name>
<description>Minimal implementation of the Minecraft protocol for use in BungeeCord</description> <description>Minimal implementation of the Minecraft protocol for use in BungeeCord</description>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<!-- TODO: Fix this -->
<artifactId>netty-all</artifactId>
<version>4.0.0.Beta3-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project> </project>

View File

@ -1,14 +1,13 @@
package net.md_5.mendax; package net.md_5.bungee.protocol;
import static net.md_5.mendax.PacketDefinitions.OpCode.*; import static net.md_5.bungee.protocol.PacketDefinitions.OpCode.*;
public class PacketDefinitions public class PacketDefinitions
{ {
private static final int MAX_PACKET = 256; public static final OpCode[][] opCodes = new OpCode[ 512 ][];
public static final OpCode[][] opCodes = new OpCode[ MAX_PACKET * 2 ][];
public static final int VANILLA_PROTOCOL = 0; public static final int VANILLA_PROTOCOL = 0;
public static final int FORGE_PROTOCOL = MAX_PACKET; public static final int FORGE_PROTOCOL = 256;
public enum OpCode public enum OpCode
{ {
@ -332,7 +331,8 @@ public class PacketDefinitions
}; };
opCodes[0xFE] = new OpCode[] opCodes[0xFE] = new OpCode[]
{ {
}; // Should be byte, screw you too bitchy server admins! BYTE
};
opCodes[0xFF] = new OpCode[] opCodes[0xFF] = new OpCode[]
{ {
STRING STRING

View File

@ -1,17 +1,17 @@
package net.md_5.mendax.datainput; package net.md_5.bungee.protocol.netty;
import java.io.DataInput; import io.netty.buffer.ByteBuf;
import java.io.IOException; import java.io.IOException;
public class BulkChunk extends Instruction public class BulkChunk extends Instruction
{ {
@Override @Override
void read(DataInput in, byte[] buffer) throws IOException void read(ByteBuf in) throws IOException
{ {
short count = in.readShort(); short count = in.readShort();
int size = in.readInt(); int size = in.readInt();
in.readBoolean(); in.readBoolean();
skip( in, buffer, size + count * 12 ); in.skipBytes( size + count * 12 );
} }
} }

View File

@ -1,6 +1,6 @@
package net.md_5.mendax.datainput; package net.md_5.bungee.protocol.netty;
import java.io.DataInput; import io.netty.buffer.ByteBuf;
import java.io.IOException; import java.io.IOException;
class ByteHeader extends Instruction class ByteHeader extends Instruction
@ -14,12 +14,12 @@ class ByteHeader extends Instruction
} }
@Override @Override
void read(DataInput in, byte[] buffer) throws IOException void read(ByteBuf in) throws IOException
{ {
byte size = in.readByte(); byte size = in.readByte();
for ( byte b = 0; b < size; b++ ) for ( byte b = 0; b < size; b++ )
{ {
child.read( in, buffer ); child.read( in );
} }
} }
} }

View File

@ -1,6 +1,6 @@
package net.md_5.mendax.datainput; package net.md_5.bungee.protocol.netty;
import java.io.DataInput; import io.netty.buffer.ByteBuf;
import java.io.IOException; import java.io.IOException;
abstract class Instruction abstract class Instruction
@ -30,10 +30,5 @@ abstract class Instruction
// Custom instructions // Custom instructions
static final Instruction STRING_ARRAY = new ShortHeader( STRING ); static final Instruction STRING_ARRAY = new ShortHeader( STRING );
abstract void read(DataInput in, byte[] buffer) throws IOException; abstract void read(ByteBuf in) throws IOException;
final void skip(DataInput in, byte[] buffer, int len) throws IOException
{
in.readFully( buffer, 0, len );
}
} }

View File

@ -1,6 +1,6 @@
package net.md_5.mendax.datainput; package net.md_5.bungee.protocol.netty;
import java.io.DataInput; import io.netty.buffer.ByteBuf;
import java.io.IOException; import java.io.IOException;
class IntHeader extends Instruction class IntHeader extends Instruction
@ -14,12 +14,12 @@ class IntHeader extends Instruction
} }
@Override @Override
void read(DataInput in, byte[] buffer) throws IOException void read(ByteBuf in) throws IOException
{ {
int size = in.readInt(); int size = in.readInt();
for ( int i = 0; i < size; i++ ) for ( int i = 0; i < size; i++ )
{ {
child.read( in, buffer ); child.read( in );
} }
} }
} }

View File

@ -0,0 +1,19 @@
package net.md_5.bungee.protocol.netty;
import io.netty.buffer.ByteBuf;
import java.io.IOException;
class Item extends Instruction
{
@Override
void read(ByteBuf in) throws IOException
{
short type = in.readShort();
if ( type >= 0 )
{
in.skipBytes( 3 );
SHORT_BYTE.read( in );
}
}
}

View File

@ -1,6 +1,6 @@
package net.md_5.mendax.datainput; package net.md_5.bungee.protocol.netty;
import java.io.DataInput; import io.netty.buffer.ByteBuf;
import java.io.IOException; import java.io.IOException;
class Jump extends Instruction class Jump extends Instruction
@ -18,8 +18,8 @@ class Jump extends Instruction
} }
@Override @Override
void read(DataInput in, byte[] buffer) throws IOException void read(ByteBuf in) throws IOException
{ {
skip( in, buffer, len ); in.skipBytes( len );
} }
} }

View File

@ -1,13 +1,13 @@
package net.md_5.mendax.datainput; package net.md_5.bungee.protocol.netty;
import java.io.DataInput; import io.netty.buffer.ByteBuf;
import java.io.IOException; import java.io.IOException;
class MetaData extends Instruction class MetaData extends Instruction
{ {
@Override @Override
void read(DataInput in, byte[] buffer) throws IOException void read(ByteBuf in) throws IOException
{ {
int x = in.readUnsignedByte(); int x = in.readUnsignedByte();
while ( x != 127 ) while ( x != 127 )
@ -16,25 +16,25 @@ class MetaData extends Instruction
switch ( type ) switch ( type )
{ {
case 0: case 0:
BYTE.read( in, buffer ); BYTE.read( in );
break; break;
case 1: case 1:
SHORT.read( in, buffer ); SHORT.read( in );
break; break;
case 2: case 2:
INT.read( in, buffer ); INT.read( in );
break; break;
case 3: case 3:
FLOAT.read( in, buffer ); FLOAT.read( in );
break; break;
case 4: case 4:
STRING.read( in, buffer ); STRING.read( in );
break; break;
case 5: case 5:
ITEM.read( in, buffer ); ITEM.read( in );
break; break;
case 6: case 6:
skip( in, buffer, 12 ); // int, int, int in.skipBytes( 12 ); // int, int, int
break; break;
default: default:
throw new IllegalArgumentException( "Unknown metadata type " + type ); throw new IllegalArgumentException( "Unknown metadata type " + type );

View File

@ -0,0 +1,18 @@
package net.md_5.bungee.protocol.netty;
import io.netty.buffer.ByteBuf;
import java.io.IOException;
class OptionalMotion extends Instruction
{
@Override
void read(ByteBuf in) throws IOException
{
int data = in.readInt();
if ( data > 0 )
{
in.skipBytes( 6 );
}
}
}

View File

@ -1,16 +1,16 @@
package net.md_5.mendax.datainput; package net.md_5.bungee.protocol.netty;
import java.io.DataInput; import io.netty.buffer.ByteBuf;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import net.md_5.mendax.PacketDefinitions; import net.md_5.bungee.protocol.PacketDefinitions;
import net.md_5.mendax.PacketDefinitions.OpCode; import net.md_5.bungee.protocol.PacketDefinitions.OpCode;
public class DataInputPacketReader public class PacketReader
{ {
private static final Instruction[][] instructions = new Instruction[ 256 ][]; private static final Instruction[][] instructions = new Instruction[ PacketDefinitions.opCodes.length ][];
static static
{ {
@ -59,7 +59,7 @@ public class DataInputPacketReader
} }
} }
private static void readPacket(int packetId, DataInput in, byte[] buffer, int protocol) throws IOException private static void readPacket(int packetId, ByteBuf in, int protocol) throws IOException
{ {
Instruction[] packetDef = null; Instruction[] packetDef = null;
if ( packetId + protocol < instructions.length ) if ( packetId + protocol < instructions.length )
@ -74,20 +74,20 @@ public class DataInputPacketReader
throw new IOException( "Unknown packet id " + packetId ); throw new IOException( "Unknown packet id " + packetId );
} else } else
{ {
readPacket( packetId, in, buffer, PacketDefinitions.VANILLA_PROTOCOL ); readPacket( packetId, in, PacketDefinitions.VANILLA_PROTOCOL );
return; return;
} }
} }
for ( Instruction instruction : packetDef ) for ( Instruction instruction : packetDef )
{ {
instruction.read( in, buffer ); instruction.read( in );
} }
} }
public static void readPacket(DataInput in, byte[] buffer, int protocol) throws IOException public static void readPacket(ByteBuf in, int protocol) throws IOException
{ {
int packetId = in.readUnsignedByte(); int packetId = in.readUnsignedByte();
readPacket( packetId, in, buffer, protocol ); readPacket( packetId, in, protocol );
} }
} }

View File

@ -1,6 +1,6 @@
package net.md_5.mendax.datainput; package net.md_5.bungee.protocol.netty;
import java.io.DataInput; import io.netty.buffer.ByteBuf;
import java.io.IOException; import java.io.IOException;
class ShortHeader extends Instruction class ShortHeader extends Instruction
@ -14,12 +14,12 @@ class ShortHeader extends Instruction
} }
@Override @Override
void read(DataInput in, byte[] buffer) throws IOException void read(ByteBuf in) throws IOException
{ {
short size = in.readShort(); short size = in.readShort();
for ( short s = 0; s < size; s++ ) for ( short s = 0; s < size; s++ )
{ {
child.read( in, buffer ); child.read( in );
} }
} }
} }

View File

@ -0,0 +1,26 @@
package net.md_5.bungee.protocol.netty;
import io.netty.buffer.ByteBuf;
import java.io.IOException;
class Team extends Instruction
{
@Override
void read(ByteBuf in) throws IOException
{
STRING.read( in );
byte mode = in.readByte();
if ( mode == 0 || mode == 2 )
{
STRING.read( in );
STRING.read( in );
STRING.read( in );
BYTE.read( in );
}
if ( mode == 0 || mode == 3 || mode == 4 )
{
STRING_ARRAY.read( in );
}
}
}

View File

@ -0,0 +1,15 @@
package net.md_5.bungee.protocol.netty;
import io.netty.buffer.ByteBuf;
import java.io.IOException;
class UnsignedShortByte extends Instruction
{
@Override
void read(ByteBuf in) throws IOException
{
int size = in.readUnsignedShort();
in.skipBytes( size );
}
}

View File

@ -1,19 +0,0 @@
package net.md_5.mendax.datainput;
import java.io.DataInput;
import java.io.IOException;
class Item extends Instruction
{
@Override
void read(DataInput in, byte[] buffer) throws IOException
{
short type = in.readShort();
if ( type >= 0 )
{
skip( in, buffer, 3 );
SHORT_BYTE.read( in, buffer );
}
}
}

View File

@ -1,18 +0,0 @@
package net.md_5.mendax.datainput;
import java.io.DataInput;
import java.io.IOException;
public class OptionalMotion extends Instruction
{
@Override
void read(DataInput in, byte[] buffer) throws IOException
{
int data = in.readInt();
if ( data > 0 )
{
skip( in, buffer, 6 );
}
}
}

View File

@ -1,26 +0,0 @@
package net.md_5.mendax.datainput;
import java.io.DataInput;
import java.io.IOException;
class Team extends Instruction
{
@Override
void read(DataInput in, byte[] buffer) throws IOException
{
STRING.read( in, buffer );
byte mode = in.readByte();
if ( mode == 0 || mode == 2 )
{
STRING.read( in, buffer );
STRING.read( in, buffer );
STRING.read( in, buffer );
BYTE.read( in, buffer );
}
if ( mode == 0 || mode == 3 || mode == 4 )
{
STRING_ARRAY.read( in, buffer );
}
}
}

View File

@ -1,15 +0,0 @@
package net.md_5.mendax.datainput;
import java.io.DataInput;
import java.io.IOException;
public class UnsignedShortByte extends Instruction
{
@Override
void read(DataInput in, byte[] buffer) throws IOException
{
int size = in.readUnsignedShort();
skip( in, buffer, size );
}
}

View File

@ -19,6 +19,11 @@
<description>Proxy component of the Elastic Portal Suite</description> <description>Proxy component of the Elastic Portal Suite</description>
<dependencies> <dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.0.Beta3-SNAPSHOT</version>
</dependency>
<dependency> <dependency>
<groupId>net.md-5</groupId> <groupId>net.md-5</groupId>
<artifactId>bungeecord-protocol</artifactId> <artifactId>bungeecord-protocol</artifactId>
@ -29,6 +34,11 @@
<artifactId>bungeecord-api</artifactId> <artifactId>bungeecord-api</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>net.sf.trove4j</groupId>
<artifactId>trove4j</artifactId>
<version>3.0.3</version>
</dependency>
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>

View File

@ -1,23 +1,26 @@
package net.md_5.bungee; package net.md_5.bungee;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelException;
import io.netty.channel.MultithreadEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import net.md_5.bungee.config.Configuration; import net.md_5.bungee.config.Configuration;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -37,6 +40,7 @@ import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.plugin.PluginManager; import net.md_5.bungee.api.plugin.PluginManager;
import net.md_5.bungee.command.*; import net.md_5.bungee.command.*;
import net.md_5.bungee.config.YamlConfig; import net.md_5.bungee.config.YamlConfig;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.packet.DefinedPacket; import net.md_5.bungee.packet.DefinedPacket;
import net.md_5.bungee.packet.PacketFAPluginMessage; import net.md_5.bungee.packet.PacketFAPluginMessage;
@ -65,7 +69,7 @@ public class BungeeCord extends ProxyServer
/** /**
* Thread pool. * Thread pool.
*/ */
public final ExecutorService threadPool = Executors.newCachedThreadPool(); public final MultithreadEventLoopGroup eventLoops = new NioEventLoopGroup( 8, new ThreadFactoryBuilder().setNameFormat( "Netty IO Thread - %1$d" ).build() );
/** /**
* locations.yml save thread. * locations.yml save thread.
*/ */
@ -73,7 +77,7 @@ public class BungeeCord extends ProxyServer
/** /**
* Server socket listener. * Server socket listener.
*/ */
private Collection<ListenThread> listeners = new HashSet<>(); private Collection<Channel> listeners = new HashSet<>();
/** /**
* Fully qualified connections. * Fully qualified connections.
*/ */
@ -161,6 +165,7 @@ public class BungeeCord extends ProxyServer
* *
* @throws IOException * @throws IOException
*/ */
@Override
public void start() throws IOException public void start() throws IOException
{ {
File plugins = new File( "plugins" ); File plugins = new File( "plugins" );
@ -193,30 +198,28 @@ public class BungeeCord extends ProxyServer
{ {
for ( ListenerInfo info : config.getListeners() ) for ( ListenerInfo info : config.getListeners() )
{ {
try Channel server = new ServerBootstrap()
{ .channel( NioServerSocketChannel.class )
ListenThread listener = new ListenThread( info ); .childAttr( PipelineUtils.LISTENER, info )
listener.start(); .childHandler( PipelineUtils.SERVER_CHILD )
listeners.add( listener ); .group( eventLoops )
.localAddress( info.getHost() )
.bind().channel();
listeners.add( server );
$().info( "Listening on " + info.getHost() ); $().info( "Listening on " + info.getHost() );
} catch ( IOException ex )
{
$().log( Level.SEVERE, "Could not start listener " + info, ex );
}
} }
} }
public void stopListeners() public void stopListeners()
{ {
for ( ListenThread listener : listeners ) for ( Channel listener : listeners )
{ {
$().log( Level.INFO, "Closing listen thread {0}", listener.socket ); $().log( Level.INFO, "Closing listener {0}", listener );
try try
{ {
listener.interrupt(); listener.close().syncUninterruptibly();
listener.socket.close(); } catch ( ChannelException ex )
listener.join();
} catch ( InterruptedException | IOException ex )
{ {
$().severe( "Could not close listen thread" ); $().severe( "Could not close listen thread" );
} }
@ -231,7 +234,6 @@ public class BungeeCord extends ProxyServer
stopListeners(); stopListeners();
$().info( "Closing pending connections" ); $().info( "Closing pending connections" );
threadPool.shutdown();
$().info( "Disconnecting " + connections.size() + " connections" ); $().info( "Disconnecting " + connections.size() + " connections" );
for ( UserConnection user : connections.values() ) for ( UserConnection user : connections.values() )
@ -239,6 +241,9 @@ public class BungeeCord extends ProxyServer
user.disconnect( "Proxy restarting, brb." ); user.disconnect( "Proxy restarting, brb." );
} }
$().info( "Closing IO threads" );
eventLoops.shutdown();
$().info( "Saving reconnect locations" ); $().info( "Saving reconnect locations" );
reconnectHandler.save(); reconnectHandler.save();
saveThread.cancel(); saveThread.cancel();
@ -253,20 +258,6 @@ public class BungeeCord extends ProxyServer
System.exit( 0 ); 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. * Broadcasts a packet to all clients that is connected to this instance.
* *
@ -276,7 +267,7 @@ public class BungeeCord extends ProxyServer
{ {
for ( UserConnection con : connections.values() ) for ( UserConnection con : connections.values() )
{ {
con.packetQueue.add( packet ); con.sendPacket( packet );
} }
} }

View File

@ -1,8 +1,11 @@
package net.md_5.bungee; package net.md_5.bungee;
import java.io.DataOutputStream; import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import lombok.Getter; import lombok.Getter;
@ -11,11 +14,11 @@ import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.ServerPing; import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.Server; import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.connection.PingHandler;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.packet.DefinedPacket; import net.md_5.bungee.packet.DefinedPacket;
import net.md_5.bungee.packet.PacketFAPluginMessage; import net.md_5.bungee.packet.PacketFAPluginMessage;
import net.md_5.bungee.packet.PacketFFKick;
import net.md_5.bungee.packet.PacketStream;
import net.md_5.mendax.PacketDefinitions;
public class BungeeServerInfo extends ServerInfo public class BungeeServerInfo extends ServerInfo
{ {
@ -44,31 +47,24 @@ public class BungeeServerInfo extends ServerInfo
@Override @Override
public void ping(final Callback<ServerPing> callback) public void ping(final Callback<ServerPing> callback)
{ {
new Thread() new Bootstrap()
.channel( NioSocketChannel.class )
.group( BungeeCord.getInstance().eventLoops )
.handler( PipelineUtils.BASE )
.option( ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000 ) // TODO: Configurable
.remoteAddress( getAddress() )
.connect()
.addListener( new ChannelFutureListener()
{ {
@Override @Override
public void run() public void operationComplete(ChannelFuture future) throws Exception
{ {
try ( Socket socket = new Socket(); ) if ( !future.isSuccess() )
{ {
socket.connect( getAddress() ); callback.done( null, future.cause() );
DataOutputStream out = new DataOutputStream( socket.getOutputStream() );
out.write( 0xFE );
out.write( 0x01 );
PacketStream in = new PacketStream( socket.getInputStream(), PacketDefinitions.VANILLA_PROTOCOL );
PacketFFKick response = new PacketFFKick( in.readPacket() );
String[] split = response.message.split( "\00" );
ServerPing ping = new ServerPing( Byte.parseByte( split[1] ), split[2], split[3], Integer.parseInt( split[4] ), Integer.parseInt( split[5] ) );
callback.done( ping, null );
} catch ( Throwable t )
{
callback.done( null, t );
} }
} }
}.start(); } )
.channel().pipeline().get( HandlerBoss.class ).setHandler( new PingHandler( this, callback ) );
} }
} }

View File

@ -1,17 +1,10 @@
package net.md_5.bungee; 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.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.Key; import java.security.Key;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Random; import java.util.Random;
@ -32,7 +25,7 @@ public class EncryptionUtil
{ {
private static final Random random = new Random(); private static final Random random = new Random();
private static KeyPair keys; public static KeyPair keys;
public static PacketFDEncryptionRequest encryptRequest() throws NoSuchAlgorithmException public static PacketFDEncryptionRequest encryptRequest() throws NoSuchAlgorithmException
{ {
@ -66,30 +59,6 @@ public class EncryptionUtil
return new SecretKeySpec( secret, "AES" ); 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 Cipher getCipher(int opMode, Key shared) throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException public static Cipher getCipher(int opMode, Key shared) throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException
{ {
Cipher cip = Cipher.getInstance( "AES/CFB8/NoPadding" ); Cipher cip = Cipher.getInstance( "AES/CFB8/NoPadding" );

View File

@ -1,5 +1,7 @@
package net.md_5.bungee; package net.md_5.bungee;
import io.netty.buffer.ByteBuf;
/** /**
* Class to rewrite integers within packets. * Class to rewrite integers within packets.
*/ */
@ -113,20 +115,20 @@ public class EntityMap
}; };
} }
public static void rewrite(byte[] packet, int oldId, int newId) public static void rewrite(ByteBuf packet, int oldId, int newId)
{ {
int packetId = Util.getId( packet ); int packetId = packet.getUnsignedByte( 0 );
if ( packetId == 0x1D ) if ( packetId == 0x1D )
{ // bulk entity { // bulk entity
for ( int pos = 2; pos < packet.length; pos += 4 ) for ( int pos = 2; pos < packet.writerIndex(); pos += 4 )
{ {
int readId = readInt( packet, pos ); int readId = packet.getInt( pos );
if ( readId == oldId ) if ( readId == oldId )
{ {
setInt( packet, pos, newId ); packet.setInt( pos, newId );
} else if ( readId == newId ) } else if ( readId == newId )
{ {
setInt( packet, pos, oldId ); packet.setInt( pos, oldId );
} }
} }
} else } else
@ -136,29 +138,16 @@ public class EntityMap
{ {
for ( int pos : idArray ) for ( int pos : idArray )
{ {
int readId = readInt( packet, pos ); int readId = packet.getInt( pos );
if ( readId == oldId ) if ( readId == oldId )
{ {
setInt( packet, pos, newId ); packet.setInt( pos, newId );
} else if ( readId == newId ) } else if ( readId == newId )
{ {
setInt( packet, pos, oldId ); packet.setInt( pos, oldId );
} }
} }
} }
} }
} }
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 );
}
} }

View File

@ -1,61 +0,0 @@
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.PacketStream;
/**
* Class to represent a Minecraft connection.
*/
@EqualsAndHashCode
@RequiredArgsConstructor
public class GenericConnection
{
protected final Socket socket;
protected final PacketStream stream;
@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
{
stream.write( new PacketFFKick( "[Proxy] " + reason ) );
} catch ( IOException ex )
{
} finally
{
try
{
socket.shutdownOutput();
socket.close();
} catch ( IOException ioe )
{
}
}
}
public void log(String message)
{
$().info( socket.getInetAddress() + ( ( name == null ) ? " " : " [" + name + "] " ) + message );
}
}

View File

@ -1,232 +0,0 @@
package net.md_5.bungee;
import com.google.common.base.Preconditions;
import java.io.EOFException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
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.ServerPing;
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.api.event.ProxyPingEvent;
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.PacketFEPing;
import net.md_5.bungee.packet.PacketFFKick;
import net.md_5.bungee.packet.PacketHandler;
import net.md_5.bungee.packet.PacketStream;
import net.md_5.mendax.PacketDefinitions;
public class InitialHandler extends PacketHandler implements Runnable, PendingConnection
{
private final Socket socket;
@Getter
private final ListenerInfo listener;
private PacketStream stream;
private Packet1Login forgeLogin;
private Packet2Handshake handshake;
private PacketFDEncryptionRequest request;
private List<PacketFAPluginMessage> loginMessages = new ArrayList<>();
private State thisState = State.HANDSHAKE;
private static final PacketFAPluginMessage forgeMods = new PacketFAPluginMessage( "FML", new byte[]
{
0, 0, 0, 0, 0, 2
} );
public InitialHandler(Socket socket, ListenerInfo info) throws IOException
{
this.socket = socket;
this.listener = info;
stream = new PacketStream( socket.getInputStream(), socket.getOutputStream(), PacketDefinitions.VANILLA_PROTOCOL );
}
private enum State
{
HANDSHAKE, ENCRYPT, LOGIN, FINISHED;
}
@Override
public void handle(Packet1Login login) throws Exception
{
Preconditions.checkState( thisState == State.LOGIN, "Not expecting FORGE LOGIN" );
Preconditions.checkState( forgeLogin == null, "Already received FORGE LOGIN" );
forgeLogin = login;
stream.setProtocol( PacketDefinitions.FORGE_PROTOCOL );
}
@Override
public void handle(PacketFAPluginMessage pluginMessage) throws Exception
{
loginMessages.add( pluginMessage );
}
@Override
public void handle(PacketFEPing ping) throws Exception
{
socket.setSoTimeout( 100 );
boolean newPing = false;
try
{
socket.getInputStream().read();
newPing = true;
} catch ( IOException ex )
{
}
ServerPing pingevent = new ServerPing( BungeeCord.PROTOCOL_VERSION, BungeeCord.GAME_VERSION,
listener.getMotd(), ProxyServer.getInstance().getPlayers().size(), listener.getMaxPlayers() );
pingevent = ProxyServer.getInstance().getPluginManager().callEvent( new ProxyPingEvent( this, pingevent ) ).getResponse();
String response = ( newPing ) ? ChatColor.COLOR_CHAR + "1"
+ "\00" + pingevent.getProtocolVersion()
+ "\00" + pingevent.getGameVersion()
+ "\00" + pingevent.getMotd()
+ "\00" + pingevent.getCurrentPlayers()
+ "\00" + pingevent.getMaxPlayers()
: pingevent.getMotd() + ChatColor.COLOR_CHAR + pingevent.getCurrentPlayers() + ChatColor.COLOR_CHAR + pingevent.getMaxPlayers();
disconnect( response );
}
@Override
public void handle(Packet2Handshake handshake) throws Exception
{
Preconditions.checkState( thisState == State.HANDSHAKE, "Not expecting HANDSHAKE" );
this.handshake = handshake;
stream.write( forgeMods );
stream.write( request = EncryptionUtil.encryptRequest() );
thisState = State.ENCRYPT;
}
@Override
public void handle(PacketFCEncryptionResponse encryptResponse) throws Exception
{
Preconditions.checkState( thisState == State.ENCRYPT, "Not expecting ENCRYPT" );
SecretKey shared = EncryptionUtil.getSecret( encryptResponse, request );
if ( BungeeCord.getInstance().config.isOnlineMode() && !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 );
ProxyServer.getInstance().getPluginManager().callEvent( event );
if ( event.isCancelled() )
{
throw new KickException( event.getCancelReason() );
}
stream.write( new PacketFCEncryptionResponse() );
stream = new PacketStream( new CipherInputStream( socket.getInputStream(), EncryptionUtil.getCipher( Cipher.DECRYPT_MODE, shared ) ),
new CipherOutputStream( socket.getOutputStream(), EncryptionUtil.getCipher( Cipher.ENCRYPT_MODE, shared ) ), stream.getProtocol() );
thisState = State.LOGIN;
}
@Override
public void handle(PacketCDClientStatus clientStatus) throws Exception
{
Preconditions.checkState( thisState == State.LOGIN, "Not expecting LOGIN" );
UserConnection userCon = new UserConnection( socket, this, stream, handshake, forgeLogin, loginMessages );
ServerInfo server = ProxyServer.getInstance().getReconnectHandler().getServer( userCon );
userCon.connect( server, true );
thisState = State.FINISHED;
}
@Override
public void run()
{
try
{
while ( thisState != State.FINISHED )
{
byte[] buf = stream.readPacket();
DefinedPacket packet = DefinedPacket.packet( buf );
packet.handle( this );
}
} catch ( KickException ex )
{
disconnect( "[Proxy - Kicked] " + ex.getMessage() );
} catch ( EOFException ex )
{
} catch ( Exception ex )
{
disconnect( "[Proxy Error] " + Util.exception( ex ) );
ex.printStackTrace();
}
}
@Override
public void disconnect(String reason)
{
thisState = State.FINISHED;
try
{
stream.write( new PacketFFKick( reason ) );
} catch ( IOException ioe )
{
} finally
{
try
{
socket.shutdownOutput();
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();
}
}

View File

@ -1,14 +0,0 @@
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 );
}
}

View File

@ -1,48 +0,0 @@
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 ( !isInterrupted() )
{
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 )
{
ex.printStackTrace(); // Now people can see why their operating system is failing them and stop bitching at me!
} catch ( IOException ex )
{
ex.printStackTrace(); // TODO
}
}
}
}

View File

@ -3,7 +3,6 @@ package net.md_5.bungee;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.text.MessageFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.logging.FileHandler; import java.util.logging.FileHandler;
import java.util.logging.Formatter; import java.util.logging.Formatter;
@ -21,7 +20,7 @@ public class Logger extends java.util.logging.Logger
public Logger() public Logger()
{ {
super( "RubberBand", null ); super( "BungeeCord", null );
try try
{ {
FileHandler handler = new FileHandler( "proxy.log", 1 << 14, 1, true ); FileHandler handler = new FileHandler( "proxy.log", 1 << 14, 1, true );

View File

@ -1,47 +1,57 @@
package net.md_5.bungee; package net.md_5.bungee;
import io.netty.channel.Channel;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.util.concurrent.TimeUnit;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import lombok.Getter; import lombok.Getter;
import net.md_5.bungee.api.Callback; import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ServerPing; import lombok.Setter;
import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.Server; import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.packet.DefinedPacket;
import net.md_5.bungee.packet.Packet1Login; import net.md_5.bungee.packet.Packet1Login;
import net.md_5.bungee.packet.PacketFAPluginMessage; import net.md_5.bungee.packet.PacketFAPluginMessage;
import net.md_5.bungee.packet.PacketStream; import net.md_5.bungee.packet.PacketFFKick;
/** @RequiredArgsConstructor
* Class representing a connection from the proxy to the server; ie upstream. public class ServerConnection implements Server
*/
public class ServerConnection extends GenericConnection implements Server
{ {
@Getter
private final Channel ch;
@Getter @Getter
private final ServerInfo info; private final ServerInfo info;
public final Packet1Login loginPacket; @Getter
public Queue<DefinedPacket> packetQueue = new ConcurrentLinkedQueue<>(); private final Packet1Login loginPacket;
@Getter
public ServerConnection(Socket socket, ServerInfo info, PacketStream stream, Packet1Login loginPacket) @Setter
{ private boolean isObsolete;
super( socket, stream );
this.info = info;
this.loginPacket = loginPacket;
}
@Override @Override
public void sendData(String channel, byte[] data) public void sendData(String channel, byte[] data)
{ {
packetQueue.add( new PacketFAPluginMessage( channel, data ) ); ch.write( new PacketFAPluginMessage( channel, data ) );
} }
@Override @Override
public void ping(final Callback<ServerPing> callback) public synchronized void disconnect(String reason)
{ {
getInfo().ping( callback ); disconnect( ch, reason );
}
static void disconnect(final Channel ch, String reason)
{
if ( ch.isActive() )
{
ch.write( new PacketFFKick( reason ) );
ch.eventLoop().schedule( new Runnable()
{
@Override
public void run()
{
ch.close();
}
}, 100, TimeUnit.MILLISECONDS );
}
} }
@Override @Override

View File

@ -1,45 +1,119 @@
package net.md_5.bungee; package net.md_5.bungee;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import java.io.IOException; import io.netty.channel.Channel;
import java.net.Socket;
import java.util.Queue; import java.util.Queue;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.event.ServerConnectedEvent; import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.connection.CancelSendSignal;
import net.md_5.bungee.connection.DownstreamBridge;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.packet.DefinedPacket; import net.md_5.bungee.packet.DefinedPacket;
import net.md_5.bungee.packet.Packet1Login; import net.md_5.bungee.packet.Packet1Login;
import net.md_5.bungee.packet.Packet2Handshake;
import net.md_5.bungee.packet.Packet9Respawn;
import net.md_5.bungee.packet.PacketCDClientStatus; import net.md_5.bungee.packet.PacketCDClientStatus;
import net.md_5.bungee.packet.PacketFDEncryptionRequest; import net.md_5.bungee.packet.PacketFDEncryptionRequest;
import net.md_5.bungee.packet.PacketFFKick; import net.md_5.bungee.packet.PacketFFKick;
import net.md_5.bungee.packet.PacketHandler; import net.md_5.bungee.packet.PacketHandler;
import net.md_5.bungee.packet.PacketStream;
@RequiredArgsConstructor
public class ServerConnector extends PacketHandler public class ServerConnector extends PacketHandler
{ {
private final PacketStream stream; private final ProxyServer bungee;
private Packet1Login loginPacket; private Channel ch;
private final UserConnection user;
private final ServerInfo target;
private State thisState = State.ENCRYPT_REQUEST; private State thisState = State.ENCRYPT_REQUEST;
public ServerConnector(PacketStream stream)
{
this.stream = stream;
}
private enum State private enum State
{ {
ENCRYPT_REQUEST, LOGIN, FINISHED; ENCRYPT_REQUEST, LOGIN, FINISHED;
} }
@Override
public void connected(Channel channel) throws Exception
{
this.ch = channel;
// TODO: Fix this crap
channel.write( new Packet2Handshake( user.handshake.procolVersion, user.handshake.username, user.handshake.host, user.handshake.port ) );
channel.write( PacketCDClientStatus.CLIENT_LOGIN );
}
@Override @Override
public void handle(Packet1Login login) throws Exception public void handle(Packet1Login login) throws Exception
{ {
Preconditions.checkState( thisState == State.LOGIN, "Not exepcting LOGIN" ); Preconditions.checkState( thisState == State.LOGIN, "Not exepcting LOGIN" );
loginPacket = login;
ServerConnection server = new ServerConnection( ch, target, login );
ServerConnectedEvent event = new ServerConnectedEvent( user, server );
bungee.getPluginManager().callEvent( event );
ch.write( BungeeCord.getInstance().registerChannels() );
Queue<DefinedPacket> packetQueue = ( (BungeeServerInfo) target ).getPacketQueue();
while ( !packetQueue.isEmpty() )
{
ch.write( packetQueue.poll() );
}
synchronized ( user.getSwitchMutex() )
{
if ( user.getServer() == null )
{
BungeeCord.getInstance().connections.put( user.getName(), user );
bungee.getTabListHandler().onConnect( user );
// Once again, first connection
user.clientEntityId = login.entityId;
user.serverEntityId = login.entityId;
// Set tab list size
Packet1Login modLogin = new Packet1Login(
login.entityId,
login.levelType,
login.gameMode,
(byte) login.dimension,
login.difficulty,
login.unused,
(byte) user.getPendingConnection().getListener().getTabListSize() );
user.ch.write( modLogin );
ch.write( BungeeCord.getInstance().registerChannels() );
} else
{
bungee.getTabListHandler().onServerChange( user );
user.sendPacket( Packet9Respawn.DIM1_SWITCH );
user.sendPacket( Packet9Respawn.DIM2_SWITCH );
user.serverEntityId = login.entityId;
user.ch.write( new Packet9Respawn( login.dimension, login.difficulty, login.gameMode, (short) 256, login.levelType ) );
// Remove from old servers
user.getServer().setObsolete( true );
user.getServer().disconnect( "Quitting" );
}
// TODO: Fix this?
if ( !user.ch.isActive() )
{
server.disconnect( "Quitting" );
throw new IllegalStateException( "No client connected for pending server!" );
}
// Add to new server
// TODO: Move this to the connected() method of DownstreamBridge
target.addPlayer( user );
user.setServer( server );
ch.pipeline().get( HandlerBoss.class ).setHandler( new DownstreamBridge( bungee, user, server ) );
}
thisState = State.FINISHED; thisState = State.FINISHED;
throw new CancelSendSignal();
} }
@Override @Override
@ -52,66 +126,19 @@ public class ServerConnector extends PacketHandler
@Override @Override
public void handle(PacketFFKick kick) throws Exception public void handle(PacketFFKick kick) throws Exception
{ {
throw new KickException( kick.message ); String message = ChatColor.RED + "Kicked whilst connecting to " + target.getName() + ": " + kick.message;
} if ( user.getServer() == null )
public static ServerConnection connect(UserConnection user, ServerInfo info, boolean retry)
{ {
Socket socket = null; user.disconnect( message );
try
{
socket = new Socket();
socket.connect( info.getAddress(), BungeeCord.getInstance().config.getTimeout() );
BungeeCord.getInstance().setSocketOptions( socket );
PacketStream stream = new PacketStream( socket.getInputStream(), socket.getOutputStream(), user.stream.getProtocol() );
ServerConnector connector = new ServerConnector( stream );
stream.write( user.handshake );
stream.write( PacketCDClientStatus.CLIENT_LOGIN );
while ( connector.thisState != State.FINISHED )
{
byte[] buf = stream.readPacket();
DefinedPacket packet = DefinedPacket.packet( buf );
packet.handle( connector );
}
ServerConnection server = new ServerConnection( socket, info, stream, connector.loginPacket );
ServerConnectedEvent event = new ServerConnectedEvent( user, server );
ProxyServer.getInstance().getPluginManager().callEvent( event );
stream.write( BungeeCord.getInstance().registerChannels() );
Queue<DefinedPacket> packetQueue = ( (BungeeServerInfo) info ).getPacketQueue();
while ( !packetQueue.isEmpty() )
{
stream.write( packetQueue.poll() );
}
return server;
} catch ( Exception ex )
{
if ( socket != null )
{
try
{
socket.close();
} catch ( IOException ioe )
{
}
}
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, false );
} else } else
{ {
if ( ex instanceof RuntimeException ) user.sendMessage( message );
}
}
@Override
public String toString()
{ {
throw (RuntimeException) ex; return "[" + user.getName() + "] <-> ServerConnector [" + target.getName() + "]";
}
throw new RuntimeException( "Could not connect to target server " + Util.exception( ex ) );
}
}
} }
} }

View File

@ -1,214 +1,188 @@
package net.md_5.bungee; package net.md_5.bungee;
import java.io.ByteArrayInputStream; import com.google.common.base.Preconditions;
import java.io.ByteArrayOutputStream; import gnu.trove.set.hash.THashSet;
import java.io.DataInputStream; import io.netty.bootstrap.Bootstrap;
import java.io.DataOutputStream; import io.netty.channel.Channel;
import java.io.IOException; import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.logging.Level;
import java.util.Queue; import lombok.AccessLevel;
import java.util.concurrent.ConcurrentLinkedQueue;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import lombok.Synchronized; import lombok.Synchronized;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.PendingConnection; import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.ChatEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.ServerConnectEvent; import net.md_5.bungee.api.event.ServerConnectEvent;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.packet.*; import net.md_5.bungee.packet.*;
public final class UserConnection extends GenericConnection implements ProxiedPlayer public final class UserConnection implements ProxiedPlayer
{ {
public final Packet2Handshake handshake; public final Packet2Handshake handshake;
private final ProxyServer bungee;
public final Channel ch;
final Packet1Login forgeLogin; final Packet1Login forgeLogin;
final List<PacketFAPluginMessage> loginMessages; final List<PacketFAPluginMessage> loginMessages;
public Queue<DefinedPacket> packetQueue = new ConcurrentLinkedQueue<>();
@Getter @Getter
private final PendingConnection pendingConnection; private final PendingConnection pendingConnection;
@Getter @Getter
@Setter(AccessLevel.PACKAGE)
private ServerConnection server; private ServerConnection server;
private UpstreamBridge upBridge;
private DownstreamBridge downBridge;
// reconnect stuff // reconnect stuff
private int clientEntityId; public int clientEntityId;
private int serverEntityId; public int serverEntityId;
private volatile boolean reconnecting;
// ping stuff // ping stuff
private int trackingPingId; public int trackingPingId;
private long pingTime; public long pingTime;
@Getter @Getter
private String name;
@Getter
private String displayName;
@Getter
@Setter
private int ping = 1000; private int ping = 1000;
// Permissions // Permissions
private final Collection<String> groups = new HashSet<>(); private final Collection<String> playerGroups = new THashSet<>();
private final Map<String, Boolean> permissions = new HashMap<>(); private final Collection<String> permissions = new THashSet<>();
private final Object permMutex = new Object(); private final Object permMutex = new Object();
// Hack for connect timings @Getter
private ServerInfo nextServer; private final Object switchMutex = new Object();
private volatile boolean clientConnected = true;
public UserConnection(Socket socket, PendingConnection pendingConnection, PacketStream stream, Packet2Handshake handshake, Packet1Login forgeLogin, List<PacketFAPluginMessage> loginMessages) public UserConnection(BungeeCord bungee, Channel channel, PendingConnection pendingConnection, Packet2Handshake handshake, Packet1Login forgeLogin, List<PacketFAPluginMessage> loginMessages)
{ {
super( socket, stream ); this.bungee = bungee;
this.ch = channel;
this.handshake = handshake; this.handshake = handshake;
this.pendingConnection = pendingConnection; this.pendingConnection = pendingConnection;
this.forgeLogin = forgeLogin; this.forgeLogin = forgeLogin;
this.loginMessages = loginMessages; this.loginMessages = loginMessages;
name = handshake.username.substring( 0, Math.min( handshake.username.length(), 16 ) ); this.name = handshake.username;
displayName = name; this.displayName = name;
Collection<String> g = ProxyServer.getInstance().getConfigurationAdapter().getGroups( name ); Collection<String> g = bungee.getConfigurationAdapter().getGroups( name );
for ( String s : g ) for ( String s : g )
{ {
addGroups( s ); addGroups( s );
} }
} }
public void sendPacket(DefinedPacket p)
{
ch.write( p );
}
@Override @Override
public void setDisplayName(String name) public void setDisplayName(String name)
{ {
ProxyServer.getInstance().getTabListHandler().onDisconnect( this ); Preconditions.checkArgument( name.length() <= 16, "Display name cannot be longer than 16 characters" );
displayName = name; bungee.getTabListHandler().onDisconnect( this );
ProxyServer.getInstance().getTabListHandler().onConnect( this ); bungee.getTabListHandler().onConnect( this );
} }
@Override @Override
public void connect(ServerInfo target) public void connect(ServerInfo target)
{ {
nextServer = target; connect( target, false );
} }
public void connect(ServerInfo target, boolean force) public void connect(ServerInfo info, final boolean retry)
{ {
nextServer = null; ServerConnectEvent event = new ServerConnectEvent( this, info );
if ( server == null ) ProxyServer.getInstance().getPluginManager().callEvent( event );
final ServerInfo target = event.getTarget(); // Update in case the event changed target
new Bootstrap()
.channel( NioSocketChannel.class )
.group( BungeeCord.getInstance().eventLoops )
.handler( new ChannelInitializer()
{ {
// First join @Override
BungeeCord.getInstance().connections.put( name, this ); protected void initChannel(Channel ch) throws Exception
ProxyServer.getInstance().getTabListHandler().onConnect( this ); {
PipelineUtils.BASE.initChannel( ch );
ch.pipeline().get( HandlerBoss.class ).setHandler( new ServerConnector( bungee, UserConnection.this, target ) );
} }
} )
ServerConnectEvent event = new ServerConnectEvent( this, target ); .option( ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000 ) // TODO: Configurable
BungeeCord.getInstance().getPluginManager().callEvent( event ); .remoteAddress( target.getAddress() )
target = event.getTarget(); // Update in case the event changed target .connect().addListener( new ChannelFutureListener()
ProxyServer.getInstance().getTabListHandler().onServerChange( this );
try
{ {
reconnecting = true; @Override
public void operationComplete(ChannelFuture future) throws Exception
if ( server != null )
{ {
stream.write( new Packet9Respawn( (byte) 1, (byte) 0, (byte) 0, (short) 256, "DEFAULT" ) ); if ( !future.isSuccess() )
stream.write( new Packet9Respawn( (byte) -1, (byte) 0, (byte) 0, (short) 256, "DEFAULT" ) );
}
ServerConnection newServer = ServerConnector.connect( this, target, true );
if ( server == null )
{ {
// Once again, first connection future.channel().close();
clientEntityId = newServer.loginPacket.entityId; ServerInfo def = ProxyServer.getInstance().getServers().get( getPendingConnection().getListener().getDefaultServer() );
serverEntityId = newServer.loginPacket.entityId; if ( retry && !target.equals( def ) )
// Set tab list size {
Packet1Login s = newServer.loginPacket; sendMessage( ChatColor.RED + "Could not connect to target server, you have been moved to the default server" );
Packet1Login login = new Packet1Login( s.entityId, s.levelType, s.gameMode, (byte) s.dimension, s.difficulty, s.unused, (byte) pendingConnection.getListener().getTabListSize() ); connect( def, false );
stream.write( login );
stream.write( BungeeCord.getInstance().registerChannels() );
upBridge = new UpstreamBridge();
upBridge.start();
} else } else
{ {
try if ( server == null )
{ {
downBridge.interrupt(); disconnect( "Server down, could not connect to default! " + future.cause().getClass().getName() );
downBridge.join(); } else
} catch ( InterruptedException ie )
{ {
sendMessage( ChatColor.RED + "Could not connect to target server: " + future.cause().getClass().getName() );
} }
server.disconnect( "Quitting" );
server.getInfo().removePlayer( this );
Packet1Login login = newServer.loginPacket;
serverEntityId = login.entityId;
stream.write( new Packet9Respawn( login.dimension, login.difficulty, login.gameMode, (short) 256, login.levelType ) );
} }
// 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 )
{
disconnect( ex.getMessage() );
} catch ( Exception ex )
{
disconnect( "Could not connect to server - " + Util.exception( ex ) );
} }
} }
} );
}
@Override @Override
public synchronized void disconnect(String reason) public synchronized void disconnect(String reason)
{ {
if ( clientConnected ) if ( ch.isActive() )
{ {
PlayerDisconnectEvent event = new PlayerDisconnectEvent( this ); bungee.getLogger().log( Level.INFO, "[" + getName() + "] disconnected with: " + reason );
ProxyServer.getInstance().getPluginManager().callEvent( event ); ch.write( new PacketFFKick( reason ) );
ProxyServer.getInstance().getTabListHandler().onDisconnect( this ); ch.close();
ProxyServer.getInstance().getPlayers().remove( this );
super.disconnect( reason );
if ( server != null ) if ( server != null )
{ {
server.getInfo().removePlayer( this );
server.disconnect( "Quitting" ); server.disconnect( "Quitting" );
ProxyServer.getInstance().getReconnectHandler().setServer( this );
} }
clientConnected = false;
} }
} }
@Override @Override
public void sendMessage(String message) public void sendMessage(String message)
{ {
packetQueue.add( new Packet3Chat( message ) ); ch.write( new Packet3Chat( message ) );
} }
@Override @Override
public void sendData(String channel, byte[] data) public void sendData(String channel, byte[] data)
{ {
server.packetQueue.add( new PacketFAPluginMessage( channel, data ) ); ch.write( new PacketFAPluginMessage( channel, data ) );
} }
@Override @Override
public InetSocketAddress getAddress() public InetSocketAddress getAddress()
{ {
return (InetSocketAddress) socket.getRemoteSocketAddress(); return (InetSocketAddress) ch.remoteAddress();
} }
@Override @Override
@Synchronized("permMutex") @Synchronized("permMutex")
public Collection<String> getGroups() public Collection<String> getGroups()
{ {
return Collections.unmodifiableCollection( groups ); return Collections.unmodifiableCollection( playerGroups );
} }
@Override @Override
@ -217,8 +191,8 @@ public final class UserConnection extends GenericConnection implements ProxiedPl
{ {
for ( String group : groups ) for ( String group : groups )
{ {
this.groups.add( group ); playerGroups.add( group );
for ( String permission : ProxyServer.getInstance().getConfigurationAdapter().getPermissions( group ) ) for ( String permission : bungee.getConfigurationAdapter().getPermissions( group ) )
{ {
setPermission( permission, true ); setPermission( permission, true );
} }
@ -231,8 +205,8 @@ public final class UserConnection extends GenericConnection implements ProxiedPl
{ {
for ( String group : groups ) for ( String group : groups )
{ {
this.groups.remove( group ); playerGroups.remove( group );
for ( String permission : ProxyServer.getInstance().getConfigurationAdapter().getPermissions( group ) ) for ( String permission : bungee.getConfigurationAdapter().getPermissions( group ) )
{ {
setPermission( permission, false ); setPermission( permission, false );
} }
@ -243,303 +217,19 @@ public final class UserConnection extends GenericConnection implements ProxiedPl
@Synchronized("permMutex") @Synchronized("permMutex")
public boolean hasPermission(String permission) public boolean hasPermission(String permission)
{ {
Boolean val = permissions.get( permission ); return permissions.contains( permission );
return ( val == null ) ? false : val;
} }
@Override @Override
@Synchronized("permMutex") @Synchronized("permMutex")
public void setPermission(String permission, boolean value) public void setPermission(String permission, boolean value)
{ {
permissions.put( permission, value ); if ( value )
}
private class UpstreamBridge extends Thread
{ {
permissions.add( permission );
public UpstreamBridge()
{
super( "Upstream Bridge - " + name );
}
@Override
public void run()
{
while ( !socket.isClosed() )
{
try
{
byte[] packet = stream.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 } else
{ {
ChatEvent chatEvent = new ChatEvent( UserConnection.this, server, chat.message ); permissions.remove( permission );
ProxyServer.getInstance().getPluginManager().callEvent( chatEvent );
sendPacket = !chatEvent.isCancelled();
}
break;
case 0xFA:
// Call the onPluginMessage event
PacketFAPluginMessage message = new PacketFAPluginMessage( packet );
// Might matter in the future
if ( message.tag.equals( "BungeeCord" ) )
{
continue;
}
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.stream.write( p );
}
}
EntityMap.rewrite( packet, clientEntityId, serverEntityId );
if ( sendPacket && !server.socket.isClosed() )
{
server.stream.write( packet );
}
try
{
Thread.sleep( BungeeCord.getInstance().config.getSleepTime() );
} catch ( InterruptedException ex )
{
}
} catch ( IOException ex )
{
disconnect( "Reached end of stream" );
} catch ( Exception ex )
{
disconnect( 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.stream.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;
}
if ( message.tag.equals( "BungeeCord" ) )
{
String subChannel = in.readUTF();
if ( subChannel.equals( "Forward" ) )
{
String target = in.readUTF();
String channel = in.readUTF();
short len = in.readShort();
byte[] data = new byte[ len ];
in.readFully( data );
ByteArrayOutputStream b = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( b );
out.writeUTF( channel );
out.writeShort( data.length );
out.write( data );
if ( target.equals( "ALL" ) )
{
for ( ServerInfo server : BungeeCord.getInstance().getServers().values() )
{
server.sendData( "BungeeCord", b.toByteArray() );
}
} else
{
ServerInfo server = BungeeCord.getInstance().getServerInfo( target );
if ( server != null )
{
server.sendData( "BungeeCord", b.toByteArray() );
}
}
}
if ( subChannel.equals( "Connect" ) )
{
ServerInfo server = ProxyServer.getInstance().getServerInfo( in.readUTF() );
if ( server != null )
{
connect( server, true );
break outer;
}
}
if ( subChannel.equals( "IP" ) )
{
ByteArrayOutputStream b = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( b );
out.writeUTF( "IP" );
out.writeUTF( getAddress().getHostString() );
out.writeInt( getAddress().getPort() );
getServer().sendData( "BungeeCord", b.toByteArray() );
}
if ( subChannel.equals( "PlayerCount" ) )
{
ServerInfo server = ProxyServer.getInstance().getServerInfo( in.readUTF() );
if ( server != null )
{
ByteArrayOutputStream b = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( b );
out.writeUTF( "PlayerCount" );
out.writeUTF( server.getName() );
out.writeInt( server.getPlayers().size() );
getServer().sendData( "BungeeCord", b.toByteArray() );
}
}
if ( subChannel.equals( "PlayerList" ) )
{
ServerInfo server = ProxyServer.getInstance().getServerInfo( in.readUTF() );
if ( server != null )
{
ByteArrayOutputStream b = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( b );
out.writeUTF( "PlayerList" );
out.writeUTF( server.getName() );
StringBuilder sb = new StringBuilder();
for ( ProxiedPlayer p : server.getPlayers() )
{
sb.append( p.getName() );
sb.append( "," );
}
out.writeUTF( sb.substring( 0, sb.length() - 1 ) );
getServer().sendData( "BungeeCord", b.toByteArray() );
}
}
if ( subChannel.equals( "GetServers" ) )
{
ByteArrayOutputStream b = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( b );
out.writeUTF( "GetServers" );
StringBuilder sb = new StringBuilder();
for ( String server : ProxyServer.getInstance().getServers().keySet() )
{
sb.append( server );
sb.append( "," );
}
out.writeUTF( sb.substring( 0, sb.length() - 1 ) );
getServer().sendData( "BungeeCord", b.toByteArray() );
}
if ( subChannel.equals( "Message" ) )
{
ProxiedPlayer target = ProxyServer.getInstance().getPlayer( in.readUTF() );
if ( target != null )
{
target.sendMessage( in.readUTF() );
}
}
continue;
}
break;
case 0xFF:
disconnect( new PacketFFKick( packet ).message );
break outer;
}
while ( !packetQueue.isEmpty() )
{
DefinedPacket p = packetQueue.poll();
if ( p != null )
{
stream.write( p );
}
}
EntityMap.rewrite( packet, serverEntityId, clientEntityId );
stream.write( packet );
if ( nextServer != null )
{
connect( nextServer, true );
break outer;
}
}
} catch ( Exception ex )
{
disconnect( Util.exception( ex ) );
}
} }
} }
} }

View File

@ -27,18 +27,6 @@ public class Util
return new InetSocketAddress( split[0], port ); 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 * Normalizes a config path by prefix upper case letters with '_' and
* turning them to lowercase. * turning them to lowercase.

View File

@ -0,0 +1,17 @@
package net.md_5.bungee.connection;
public class CancelSendSignal extends Error
{
@Override
public synchronized Throwable initCause(Throwable cause)
{
return this;
}
@Override
public synchronized Throwable fillInStackTrace()
{
return this;
}
}

View File

@ -0,0 +1,225 @@
package net.md_5.bungee.connection;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.EntityMap;
import net.md_5.bungee.ServerConnection;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.Util;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.ChatEvent;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.packet.Packet0KeepAlive;
import net.md_5.bungee.packet.Packet3Chat;
import net.md_5.bungee.packet.PacketC9PlayerListItem;
import net.md_5.bungee.packet.PacketFAPluginMessage;
import net.md_5.bungee.packet.PacketFFKick;
import net.md_5.bungee.packet.PacketHandler;
@RequiredArgsConstructor
public class DownstreamBridge extends PacketHandler
{
private final ProxyServer bungee;
private final UserConnection con;
private final ServerConnection server;
@Override
public void exception(Throwable t) throws Exception
{
con.disconnect( Util.exception( t ) );
}
@Override
public void disconnected(Channel channel) throws Exception
{
// We lost connection to the server
server.getInfo().removePlayer( con );
bungee.getReconnectHandler().setServer( con );
if ( !server.isObsolete() )
{
con.disconnect( "[Proxy] Lost connection to server D:" );
}
}
@Override
public void handle(ByteBuf buf) throws Exception
{
EntityMap.rewrite( buf, con.serverEntityId, con.clientEntityId );
con.ch.write( buf );
}
@Override
public void handle(Packet0KeepAlive alive) throws Exception
{
con.trackingPingId = alive.id;
}
@Override
public void handle(Packet3Chat chat) throws Exception
{
ChatEvent chatEvent = new ChatEvent( con.getServer(), con, chat.message );
bungee.getPluginManager().callEvent( chatEvent );
if ( chatEvent.isCancelled() )
{
throw new CancelSendSignal();
}
}
@Override
public void handle(PacketC9PlayerListItem playerList) throws Exception
{
if ( !bungee.getTabListHandler().onListUpdate( con, playerList.username, playerList.online, playerList.ping ) )
{
throw new CancelSendSignal();
}
}
@Override
public void handle(PacketFAPluginMessage pluginMessage) throws Exception
{
ByteArrayDataInput in = ByteStreams.newDataInput( pluginMessage.data );
PluginMessageEvent event = new PluginMessageEvent( con.getServer(), con, pluginMessage.tag, pluginMessage.data.clone() );
if ( bungee.getPluginManager().callEvent( event ).isCancelled() )
{
throw new CancelSendSignal();
}
if ( pluginMessage.tag.equals( "BungeeCord" ) )
{
ByteArrayDataOutput out = ByteStreams.newDataOutput();
String subChannel = in.readUTF();
if ( subChannel.equals( "Forward" ) )
{
// Read data from server
String target = in.readUTF();
String channel = in.readUTF();
short len = in.readShort();
byte[] data = new byte[ len ];
in.readFully( data );
// Prepare new data to send
out.writeUTF( channel );
out.writeShort( data.length );
out.write( data );
byte[] payload = out.toByteArray();
// Null out stream, important as we don't want to send to ourselves
out = null;
if ( target.equals( "ALL" ) )
{
for ( ServerInfo server : bungee.getServers().values() )
{
if ( server != con.getServer().getInfo() )
{
server.sendData( "BungeeCord", payload );
}
}
} else
{
ServerInfo server = bungee.getServerInfo( target );
if ( server != null )
{
server.sendData( "BungeeCord", payload );
}
}
}
if ( subChannel.equals( "Connect" ) )
{
ServerInfo server = bungee.getServerInfo( in.readUTF() );
if ( server != null )
{
con.connect( server );
}
}
if ( subChannel.equals( "IP" ) )
{
out.writeUTF( "IP" );
out.writeUTF( con.getAddress().getHostString() );
out.writeInt( con.getAddress().getPort() );
}
if ( subChannel.equals( "PlayerCount" ) )
{
ServerInfo server = bungee.getServerInfo( in.readUTF() );
if ( server != null )
{
out.writeUTF( "PlayerCount" );
out.writeUTF( server.getName() );
out.writeInt( server.getPlayers().size() );
}
}
if ( subChannel.equals( "PlayerList" ) )
{
ServerInfo server = bungee.getServerInfo( in.readUTF() );
if ( server != null )
{
out.writeUTF( "PlayerList" );
out.writeUTF( server.getName() );
StringBuilder sb = new StringBuilder();
for ( ProxiedPlayer p : server.getPlayers() )
{
sb.append( p.getName() );
sb.append( "," );
}
out.writeUTF( sb.substring( 0, sb.length() - 1 ) );
}
}
if ( subChannel.equals( "GetServers" ) )
{
out.writeUTF( "GetServers" );
StringBuilder sb = new StringBuilder();
for ( String server : bungee.getServers().keySet() )
{
sb.append( server );
sb.append( "," );
}
out.writeUTF( sb.substring( 0, sb.length() - 1 ) );
}
if ( subChannel.equals( "Message" ) )
{
ProxiedPlayer target = bungee.getPlayer( in.readUTF() );
if ( target != null )
{
target.sendMessage( in.readUTF() );
}
}
// Check we haven't set out to null, and we have written data, if so reply back back along the BungeeCord channel
if ( out != null )
{
byte[] b = out.toByteArray();
if ( b.length != 0 )
{
con.getServer().sendData( "BungeeCord", b );
}
}
}
}
@Override
public void handle(PacketFFKick kick) throws Exception
{
con.disconnect( "[Kicked] " + kick.message );
throw new CancelSendSignal();
}
@Override
public String toString()
{
return "[" + con.getName() + "] <-> DownstreamBridge <-> [" + server.getInfo().getName() + "]";
}
}

View File

@ -0,0 +1,249 @@
package net.md_5.bungee.connection;
import com.google.common.base.Preconditions;
import io.netty.channel.Channel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.Util;
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.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.api.event.ProxyPingEvent;
import net.md_5.bungee.netty.CipherCodec;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketDecoder;
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.PacketFEPing;
import net.md_5.bungee.packet.PacketFFKick;
import net.md_5.bungee.packet.PacketHandler;
import net.md_5.bungee.protocol.PacketDefinitions;
@RequiredArgsConstructor
public class InitialHandler extends PacketHandler implements PendingConnection
{
private final ProxyServer bungee;
private Channel ch;
@Getter
private final ListenerInfo listener;
private Packet1Login forgeLogin;
private Packet2Handshake handshake;
private PacketFDEncryptionRequest request;
private List<PacketFAPluginMessage> loginMessages = new ArrayList<>();
private State thisState = State.HANDSHAKE;
private static final PacketFAPluginMessage forgeMods = new PacketFAPluginMessage( "FML", new byte[]
{
0, 0, 0, 0, 0, 2
} );
private enum State
{
HANDSHAKE, ENCRYPT, LOGIN, FINISHED;
}
@Override
public void connected(Channel channel) throws Exception
{
this.ch = channel;
}
@Override
public void handle(Packet1Login login) throws Exception
{
Preconditions.checkState( thisState == State.LOGIN, "Not expecting FORGE LOGIN" );
Preconditions.checkState( forgeLogin == null, "Already received FORGE LOGIN" );
forgeLogin = login;
ch.pipeline().get( PacketDecoder.class ).setProtocol( PacketDefinitions.FORGE_PROTOCOL );
}
@Override
public void handle(PacketFAPluginMessage pluginMessage) throws Exception
{
loginMessages.add( pluginMessage );
}
@Override
public void handle(PacketFEPing ping) throws Exception
{
ServerPing response = new ServerPing( bungee.getProtocolVersion(), bungee.getGameVersion(),
listener.getMotd(), bungee.getPlayers().size(), listener.getMaxPlayers() );
response = bungee.getPluginManager().callEvent( new ProxyPingEvent( this, response ) ).getResponse();
String kickMessage = ChatColor.DARK_BLUE
+ "\00" + response.getProtocolVersion()
+ "\00" + response.getGameVersion()
+ "\00" + response.getMotd()
+ "\00" + response.getCurrentPlayers()
+ "\00" + response.getMaxPlayers();
disconnect( kickMessage );
}
@Override
public void handle(Packet2Handshake handshake) throws Exception
{
Preconditions.checkState( thisState == State.HANDSHAKE, "Not expecting HANDSHAKE" );
Preconditions.checkArgument( handshake.username.length() <= 16, "Cannot have username longer than 16 characters" );
this.handshake = handshake;
ch.write( forgeMods );
ch.write( request = EncryptionUtil.encryptRequest() );
thisState = State.ENCRYPT;
}
@Override
public void handle(final PacketFCEncryptionResponse encryptResponse) throws Exception
{
Preconditions.checkState( thisState == State.ENCRYPT, "Not expecting ENCRYPT" );
// TODO: This is shit
new Thread( "Login Verifier - " + getName() )
{
@Override
public void run()
{
try
{
SecretKey shared = EncryptionUtil.getSecret( encryptResponse, request );
if ( BungeeCord.getInstance().config.isOnlineMode() )
{
String reply = null;
try
{
String encName = URLEncoder.encode( InitialHandler.this.getName(), "UTF-8" );
MessageDigest sha = MessageDigest.getInstance( "SHA-1" );
for ( byte[] bit : new byte[][]
{
request.serverId.getBytes( "ISO_8859_1" ), shared.getEncoded(), EncryptionUtil.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;
try ( BufferedReader in = new BufferedReader( new InputStreamReader( new URL( authURL ).openStream() ) ) )
{
reply = in.readLine();
}
} catch ( IOException ex )
{
}
if ( !"YES".equals( reply ) )
{
disconnect( "Not authenticated with Minecraft.net" );
}
// Check for multiple connections
ProxiedPlayer old = bungee.getPlayer( handshake.username );
if ( old != null )
{
old.disconnect( "You are already connected to the server" );
}
// fire login event
LoginEvent event = new LoginEvent( InitialHandler.this );
if ( bungee.getPluginManager().callEvent( event ).isCancelled() )
{
disconnect( event.getCancelReason() );
}
}
Cipher encrypt = EncryptionUtil.getCipher( Cipher.ENCRYPT_MODE, shared );
Cipher decrypt = EncryptionUtil.getCipher( Cipher.DECRYPT_MODE, shared );
ch.write( new PacketFCEncryptionResponse() );
ch.pipeline().addBefore( "decoder", "cipher", new CipherCodec( encrypt, decrypt ) );
thisState = InitialHandler.State.LOGIN;
} catch ( Throwable t )
{
disconnect( "[Report to md_5 / Server Owner] " + Util.exception( t ) );
}
}
}.start();
}
@Override
public void handle(PacketCDClientStatus clientStatus) throws Exception
{
Preconditions.checkState( thisState == State.LOGIN, "Not expecting LOGIN" );
UserConnection userCon = new UserConnection( (BungeeCord) bungee, ch, this, handshake, forgeLogin, loginMessages );
ch.pipeline().get( HandlerBoss.class ).setHandler( new UpstreamBridge( bungee, userCon ) );
ServerInfo server = bungee.getReconnectHandler().getServer( userCon );
userCon.connect( server, true );
thisState = State.FINISHED;
throw new CancelSendSignal();
}
@Override
public synchronized void disconnect(String reason)
{
if ( ch.isActive() )
{
ch.write( new PacketFFKick( reason ) );
ch.close();
}
}
@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) ch.remoteAddress();
}
@Override
public String toString()
{
return "[" + ( ( getName() != null ) ? getName() : getAddress() ) + "] <-> InitialHandler";
}
}

View File

@ -0,0 +1,49 @@
package net.md_5.bungee.connection;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.Callback;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.packet.PacketFFKick;
import net.md_5.bungee.packet.PacketHandler;
@RequiredArgsConstructor
public class PingHandler extends PacketHandler
{
private final ServerInfo target;
private final Callback<ServerPing> callback;
private static final ByteBuf pingBuf = Unpooled.wrappedBuffer( new byte[]
{
(byte) 0xFE, (byte) 0x01
} );
@Override
public void connected(Channel channel) throws Exception
{
channel.write( pingBuf );
}
@Override
public void exception(Throwable t) throws Exception
{
callback.done( null, t );
}
@Override
public void handle(PacketFFKick kick) throws Exception
{
String[] split = kick.message.split( "\00" );
ServerPing ping = new ServerPing( Byte.parseByte( split[1] ), split[2], split[3], Integer.parseInt( split[4] ), Integer.parseInt( split[5] ) );
callback.done( ping, null );
}
@Override
public String toString()
{
return "[Ping Handler] -> " + target.getName();
}
}

View File

@ -0,0 +1,106 @@
package net.md_5.bungee.connection;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.EntityMap;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.Util;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.event.ChatEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.packet.Packet0KeepAlive;
import net.md_5.bungee.packet.Packet3Chat;
import net.md_5.bungee.packet.PacketFAPluginMessage;
import net.md_5.bungee.packet.PacketHandler;
@RequiredArgsConstructor
public class UpstreamBridge extends PacketHandler
{
private final ProxyServer bungee;
private final UserConnection con;
@Override
public void exception(Throwable t) throws Exception
{
con.disconnect( Util.exception( t ) );
}
@Override
public void disconnected(Channel channel) throws Exception
{
// We lost connection to the client
PlayerDisconnectEvent event = new PlayerDisconnectEvent( con );
bungee.getPluginManager().callEvent( event );
bungee.getTabListHandler().onDisconnect( con );
bungee.getPlayers().remove( con );
if ( con.getServer() != null )
{
con.getServer().disconnect( "Quitting" );
}
}
@Override
public void handle(ByteBuf buf) throws Exception
{
EntityMap.rewrite( buf, con.clientEntityId, con.serverEntityId );
if ( con.getServer() != null )
{
con.getServer().getCh().write( buf );
}
}
@Override
public void handle(Packet0KeepAlive alive) throws Exception
{
if ( alive.id == con.trackingPingId )
{
int newPing = (int) ( System.currentTimeMillis() - con.pingTime );
bungee.getTabListHandler().onPingChange( con, newPing );
con.setPing( newPing );
}
}
@Override
public void handle(Packet3Chat chat) throws Exception
{
if ( chat.message.charAt( 0 ) == '/' )
{
if ( bungee.getPluginManager().dispatchCommand( con, chat.message.substring( 1 ) ) )
{
throw new CancelSendSignal();
}
} else
{
ChatEvent chatEvent = new ChatEvent( con, con.getServer(), chat.message );
if ( bungee.getPluginManager().callEvent( chatEvent ).isCancelled() )
{
throw new CancelSendSignal();
}
}
}
@Override
public void handle(PacketFAPluginMessage pluginMessage) throws Exception
{
if ( pluginMessage.tag.equals( "BungeeCord" ) )
{
throw new CancelSendSignal();
}
PluginMessageEvent event = new PluginMessageEvent( con, con.getServer(), pluginMessage.tag, pluginMessage.data.clone() );
if ( bungee.getPluginManager().callEvent( event ).isCancelled() )
{
throw new CancelSendSignal();
}
}
@Override
public String toString()
{
return "[" + con.getName() + "] -> UpstreamBridge";
}
}

View File

@ -0,0 +1,50 @@
package net.md_5.bungee.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToByteCodec;
import javax.crypto.Cipher;
import javax.crypto.ShortBufferException;
/**
* This class is a complete solution for encrypting and decoding bytes in a
* Netty stream. It takes two {@link Cipher} instances, used for encryption and
* decryption respectively.
*/
public class CipherCodec extends ByteToByteCodec
{
private Cipher encrypt;
private Cipher decrypt;
public CipherCodec(Cipher encrypt, Cipher decrypt)
{
this.encrypt = encrypt;
this.decrypt = decrypt;
}
@Override
public void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception
{
cipher( encrypt, in, out );
}
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception
{
cipher( decrypt, in, out );
}
private void cipher(Cipher cipher, ByteBuf in, ByteBuf out) throws ShortBufferException
{
int available = in.readableBytes();
int outputSize = cipher.getOutputSize( available );
if ( out.capacity() < outputSize )
{
out.capacity( outputSize );
}
int processed = cipher.update( in.nioBuffer(), out.nioBuffer( out.readerIndex(), outputSize ) );
in.readerIndex( in.readerIndex() + processed );
out.writerIndex( out.writerIndex() + processed );
}
}

View File

@ -0,0 +1,89 @@
package net.md_5.bungee.netty;
import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundMessageHandlerAdapter;
import io.netty.handler.timeout.ReadTimeoutException;
import java.util.logging.Level;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.connection.CancelSendSignal;
import net.md_5.bungee.packet.DefinedPacket;
import net.md_5.bungee.packet.PacketHandler;
/**
* This class is a primitive wrapper for {@link PacketHandler} instances tied to
* channels to maintain simple states, and only call the required, adapted
* methods when the channel is connected.
*/
public class HandlerBoss extends ChannelInboundMessageHandlerAdapter<ByteBuf>
{
private PacketHandler handler;
public void setHandler(PacketHandler handler)
{
Preconditions.checkArgument( handler != null, "handler" );
this.handler = handler;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception
{
if ( handler != null )
{
handler.connected( ctx.channel() );
ProxyServer.getInstance().getLogger().log( Level.INFO, "{0} has connected", handler );
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception
{
if ( handler != null )
{
ProxyServer.getInstance().getLogger().log( Level.INFO, "{0} has disconnected", handler );
handler.disconnected( ctx.channel() );
}
}
@Override
public void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception
{
if ( handler != null && ctx.channel().isActive() )
{
DefinedPacket packet = DefinedPacket.packet( msg );
boolean sendPacket = true;
if ( packet != null )
{
try
{
packet.handle( handler );
} catch ( CancelSendSignal ex )
{
sendPacket = false;
}
}
if ( sendPacket )
{
handler.handle( msg );
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
{
if ( ctx.channel().isActive() )
{
if ( cause instanceof ReadTimeoutException )
{
ProxyServer.getInstance().getLogger().log( Level.WARNING, handler + " - read timed out" );
} else
{
ProxyServer.getInstance().getLogger().log( Level.SEVERE, handler + " - encountered exception", cause );
}
ctx.close();
}
}
}

View File

@ -0,0 +1,34 @@
package net.md_5.bungee.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.md_5.bungee.protocol.netty.PacketReader;
/**
* This class will attempt to read a packet from {@link PacketReader}, with the
* specified {@link #protocol} before returning a new {@link ByteBuf} with the
* copied contents of all bytes read in this frame.
* <p/>
* It is based on {@link ReplayingDecoder} so that packets will only be returned
* when all needed data is present.
*/
@AllArgsConstructor
public class PacketDecoder extends ReplayingDecoder<ByteBuf>
{
@Getter
@Setter
private int protocol;
@Override
protected ByteBuf decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception
{
int startIndex = in.readerIndex();
PacketReader.readPacket( in, protocol );
return in.copy( startIndex, in.readerIndex() - startIndex );
}
}

View File

@ -0,0 +1,65 @@
package net.md_5.bungee.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelException;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.util.AttributeKey;
import java.util.concurrent.TimeUnit;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.ServerConnector;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.connection.InitialHandler;
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.protocol.PacketDefinitions;
public class PipelineUtils
{
public static final AttributeKey<ListenerInfo> LISTENER = new AttributeKey<>( "ListerInfo" );
public static final AttributeKey<UserConnection> USER = new AttributeKey<>( "User" );
public static final AttributeKey<ServerInfo> TARGET = new AttributeKey<>( "Target" );
public static final ChannelInitializer<Channel> SERVER_CHILD = new ChannelInitializer<Channel>()
{
@Override
protected void initChannel(Channel ch) throws Exception
{
BASE.initChannel( ch );
ch.pipeline().get( HandlerBoss.class ).setHandler( new InitialHandler( ProxyServer.getInstance(), ch.attr( LISTENER ).get() ) );
ch.config().setDefaultHandlerByteBufType( ChannelConfig.ChannelHandlerByteBufType.HEAP );
}
};
public static final ChannelInitializer<Channel> CLIENT = new ChannelInitializer<Channel>()
{
@Override
protected void initChannel(Channel ch) throws Exception
{
BASE.initChannel( ch );
ch.pipeline().get( HandlerBoss.class ).setHandler( new ServerConnector( ProxyServer.getInstance(), ch.attr( USER ).get(), ch.attr( TARGET ).get() ) );
}
};
public static final Base BASE = new Base();
public final static class Base extends ChannelInitializer<Channel>
{
@Override
public void initChannel(Channel ch) throws Exception
{
try
{
ch.config().setOption( ChannelOption.IP_TOS, 0x18 );
} catch ( ChannelException ex )
{
// IP_TOS is not supported (Windows XP / Windows Server 2003)
}
ch.pipeline().addLast( "timer", new ReadTimeoutHandler( BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS ) );
ch.pipeline().addLast( "decoder", new PacketDecoder( PacketDefinitions.VANILLA_PROTOCOL ) );
ch.pipeline().addLast( "handler", new HandlerBoss() );
}
};
}

View File

@ -1,12 +1,8 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import com.google.common.base.Preconditions; import io.netty.buffer.ByteBuf;
import com.google.common.io.ByteArrayDataOutput; import io.netty.buffer.ReferenceCounted;
import com.google.common.io.ByteStreams; import io.netty.buffer.Unpooled;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import lombok.Delegate; import lombok.Delegate;
@ -17,67 +13,40 @@ import net.md_5.bungee.Util;
* subclasses can read and write to the backing byte array which can be * subclasses can read and write to the backing byte array which can be
* retrieved via the {@link #getPacket()} method. * retrieved via the {@link #getPacket()} method.
*/ */
public abstract class DefinedPacket implements DataOutput public abstract class DefinedPacket implements ByteBuf
{ {
private interface Overriden @Delegate(types =
{ {
ByteBuf.class, ReferenceCounted.class
})
private ByteBuf buf;
void readUTF(); public DefinedPacket(int id, ByteBuf buf)
void writeUTF(String s);
}
private ByteArrayInputStream bin;
private DataInputStream input;
@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)
{ {
bin = new ByteArrayInputStream( buf ); this.buf = buf;
input = new DataInputStream( bin );
if ( readUnsignedByte() != id ) if ( readUnsignedByte() != id )
{ {
throw new IllegalArgumentException( "Wasn't expecting packet id " + Util.hex( id ) ); throw new IllegalArgumentException( "Wasn't expecting packet id " + Util.hex( id ) );
} }
this.id = id;
packet = buf;
} }
public DefinedPacket(int id) public DefinedPacket(int id)
{ {
out = ByteStreams.newDataOutput(); buf = Unpooled.buffer();
this.id = id;
writeByte( id ); writeByte( id );
} }
/** public void writeString(String s)
* 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 ? packet = out.toByteArray() : packet;
}
@Override
public void writeUTF(String s)
{ {
writeShort( s.length() ); writeShort( s.length() );
writeChars( s ); for ( char c : s.toCharArray() )
{
writeChar( c );
}
} }
public String readUTF() public String readString()
{ {
short len = readShort(); short len = readShort();
char[] chars = new char[ len ]; char[] chars = new char[ len ];
@ -91,99 +60,17 @@ public abstract class DefinedPacket implements DataOutput
public void writeArray(byte[] b) public void writeArray(byte[] b)
{ {
writeShort( b.length ); writeShort( b.length );
write( b ); writeBytes( b );
} }
public byte[] readArray() public byte[] readArray()
{ {
short len = readShort(); short len = readShort();
byte[] ret = new byte[ len ]; byte[] ret = new byte[ len ];
readFully( ret ); readBytes( ret );
return ret; return ret;
} }
public final int available()
{
return bin.available();
}
public final void readFully(byte b[])
{
try
{
input.readFully( b );
} catch ( IOException e )
{
throw new IllegalStateException( e );
}
}
public final boolean readBoolean()
{
try
{
return input.readBoolean();
} catch ( IOException e )
{
throw new IllegalStateException( e );
}
}
public final byte readByte()
{
try
{
return input.readByte();
} catch ( IOException e )
{
throw new IllegalStateException( e );
}
}
public final int readUnsignedByte()
{
try
{
return input.readUnsignedByte();
} catch ( IOException e )
{
throw new IllegalStateException( e );
}
}
public final short readShort()
{
try
{
return input.readShort();
} catch ( IOException e )
{
throw new IllegalStateException( e );
}
}
public final char readChar()
{
try
{
return input.readChar();
} catch ( IOException e )
{
throw new IllegalStateException( e );
}
}
public final int readInt()
{
try
{
return input.readInt();
} catch ( IOException e )
{
throw new IllegalStateException( e );
}
}
@Override @Override
public abstract boolean equals(Object obj); public abstract boolean equals(Object obj);
@ -194,28 +81,38 @@ public abstract class DefinedPacket implements DataOutput
public abstract String toString(); public abstract String toString();
public abstract void handle(PacketHandler handler) throws Exception; public abstract void handle(PacketHandler handler) throws Exception;
@SuppressWarnings("unchecked")
private static Class<? extends DefinedPacket>[] classes = new Class[ 256 ]; private static Class<? extends DefinedPacket>[] classes = new Class[ 256 ];
@SuppressWarnings("unchecked")
private static Constructor<? extends DefinedPacket>[] consructors = new Constructor[ 256 ];
public static DefinedPacket packet(byte[] buf) public static DefinedPacket packet(ByteBuf buf)
{ {
int id = Util.getId( buf ); short id = buf.getUnsignedByte( 0 );
Class<? extends DefinedPacket> clazz = classes[id]; Class<? extends DefinedPacket> clazz = classes[id];
DefinedPacket ret = null; DefinedPacket ret = null;
if ( clazz != null ) if ( clazz != null )
{ {
try try
{ {
Constructor<? extends DefinedPacket> constructor = clazz.getDeclaredConstructor( byte[].class ); Constructor<? extends DefinedPacket> constructor = consructors[id];
if ( constructor == null )
{
constructor = clazz.getDeclaredConstructor( ByteBuf.class );
consructors[id] = constructor;
}
if ( constructor != null ) if ( constructor != null )
{ {
buf.markReaderIndex();
ret = constructor.newInstance( buf ); ret = constructor.newInstance( buf );
buf.resetReaderIndex();
} }
} catch ( IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException ex ) } catch ( IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException ex )
{ {
} }
} }
Preconditions.checkState( ret != null, "Don't know how to deal with packet ID %s", Util.hex( id ) );
return ret; return ret;
} }

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -10,9 +11,9 @@ public class Packet0KeepAlive extends DefinedPacket
public int id; public int id;
public Packet0KeepAlive(byte[] buffer) Packet0KeepAlive(ByteBuf buf)
{ {
super( 0x00, buffer ); super( 0x00, buf );
id = readInt(); id = readInt();
} }

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -20,24 +21,31 @@ public class Packet1Login extends DefinedPacket
{ {
super( 0x01 ); super( 0x01 );
writeInt( entityId ); writeInt( entityId );
writeUTF( levelType ); writeString( levelType );
writeByte( gameMode ); writeByte( gameMode );
writeByte( dimension ); writeByte( dimension );
writeByte( difficulty ); writeByte( difficulty );
writeByte( unused ); writeByte( unused );
writeByte( maxPlayers ); writeByte( maxPlayers );
this.entityId = entityId;
this.levelType = levelType;
this.gameMode = gameMode;
this.dimension = dimension;
this.difficulty = difficulty;
this.unused = unused;
this.maxPlayers = maxPlayers;
} }
public Packet1Login(byte[] buf) Packet1Login(ByteBuf buf)
{ {
super( 0x01, buf ); super( 0x01, buf );
this.entityId = readInt(); this.entityId = readInt();
this.levelType = readUTF(); this.levelType = readString();
this.gameMode = readByte(); this.gameMode = readByte();
if ( available() == 4 ) if ( readableBytes() == 4 )
{ {
this.dimension = readByte(); this.dimension = readByte();
} else if ( available() == 7 ) } else if ( readableBytes() == 7 )
{ {
this.dimension = readInt(); this.dimension = readInt();
} else } else

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -17,17 +18,21 @@ public class Packet2Handshake extends DefinedPacket
{ {
super( 0x02 ); super( 0x02 );
writeByte( protocolVersion ); writeByte( protocolVersion );
writeUTF( username ); writeString( username );
writeUTF( host ); writeString( host );
writeInt( port ); writeInt( port );
this.procolVersion = protocolVersion;
this.username = username;
this.host = host;
this.port = port;
} }
public Packet2Handshake(byte[] buf) Packet2Handshake(ByteBuf buf)
{ {
super( 0x02, buf ); super( 0x02, buf );
this.procolVersion = readByte(); this.procolVersion = readByte();
this.username = readUTF(); this.username = readString();
this.host = readUTF(); this.host = readString();
this.port = readInt(); this.port = readInt();
} }

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -13,13 +14,14 @@ public class Packet3Chat extends DefinedPacket
public Packet3Chat(String message) public Packet3Chat(String message)
{ {
super( 0x03 ); super( 0x03 );
writeUTF( message ); writeString( message );
this.message = message;
} }
public Packet3Chat(byte[] buf) Packet3Chat(ByteBuf buf)
{ {
super( 0x03, buf ); super( 0x03, buf );
this.message = readUTF(); this.message = readString();
} }
@Override @Override

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -8,6 +9,8 @@ import lombok.ToString;
public class Packet9Respawn extends DefinedPacket public class Packet9Respawn extends DefinedPacket
{ {
public static final Packet9Respawn DIM1_SWITCH = new Packet9Respawn( (byte) 1, (byte) 0, (byte) 0, (short) 256, "DEFAULT" );
public static final Packet9Respawn DIM2_SWITCH = new Packet9Respawn( (byte) -1, (byte) 0, (byte) 0, (short) 256, "DEFAULT" );
public int dimension; public int dimension;
public byte difficulty; public byte difficulty;
public byte gameMode; public byte gameMode;
@ -21,17 +24,22 @@ public class Packet9Respawn extends DefinedPacket
writeByte( difficulty ); writeByte( difficulty );
writeByte( gameMode ); writeByte( gameMode );
writeShort( worldHeight ); writeShort( worldHeight );
writeUTF( levelType ); writeString( levelType );
this.dimension = dimension;
this.difficulty = difficulty;
this.gameMode = gameMode;
this.worldHeight = worldHeight;
this.levelType = levelType;
} }
public Packet9Respawn(byte[] buf) Packet9Respawn(ByteBuf buf)
{ {
super( 0x09, buf ); super( 0x09, buf );
this.dimension = readInt(); this.dimension = readInt();
this.difficulty = readByte(); this.difficulty = readByte();
this.gameMode = readByte(); this.gameMode = readByte();
this.worldHeight = readShort(); this.worldHeight = readShort();
this.levelType = readUTF(); this.levelType = readString();
} }
@Override @Override

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -12,10 +13,10 @@ public class PacketC9PlayerListItem extends DefinedPacket
public boolean online; public boolean online;
public int ping; public int ping;
public PacketC9PlayerListItem(byte[] packet) PacketC9PlayerListItem(ByteBuf buf)
{ {
super( 0xC9, packet ); super( 0xC9, buf );
username = readUTF(); username = readString();
online = readBoolean(); online = readBoolean();
ping = readShort(); ping = readShort();
} }
@ -23,7 +24,7 @@ public class PacketC9PlayerListItem extends DefinedPacket
public PacketC9PlayerListItem(String username, boolean online, int ping) public PacketC9PlayerListItem(String username, boolean online, int ping)
{ {
super( 0xC9 ); super( 0xC9 );
writeUTF( username ); writeString( username );
writeBoolean( online ); writeBoolean( online );
writeShort( ping ); writeShort( ping );
} }

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -8,6 +9,10 @@ import lombok.ToString;
public class PacketCDClientStatus extends DefinedPacket public class PacketCDClientStatus extends DefinedPacket
{ {
/**
* Represents the packet the client sends to the server when it is ready to
* login.
*/
public static PacketCDClientStatus CLIENT_LOGIN = new PacketCDClientStatus( (byte) 0 ); public static PacketCDClientStatus CLIENT_LOGIN = new PacketCDClientStatus( (byte) 0 );
/** /**
@ -21,7 +26,7 @@ public class PacketCDClientStatus extends DefinedPacket
writeByte( payload ); writeByte( payload );
} }
public PacketCDClientStatus(byte[] buf) PacketCDClientStatus(ByteBuf buf)
{ {
super( 0xCD, buf ); super( 0xCD, buf );
} }

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -14,16 +15,16 @@ public class PacketFAPluginMessage extends DefinedPacket
public PacketFAPluginMessage(String tag, byte[] data) public PacketFAPluginMessage(String tag, byte[] data)
{ {
super( 0xFA ); super( 0xFA );
writeUTF( tag ); writeString( tag );
writeArray( data ); writeArray( data );
this.tag = tag; this.tag = tag;
this.data = data; this.data = data;
} }
public PacketFAPluginMessage(byte[] buf) PacketFAPluginMessage(ByteBuf buf)
{ {
super( 0xFA, buf ); super( 0xFA, buf );
this.tag = readUTF(); this.tag = readString();
this.data = readArray(); this.data = readArray();
} }

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -23,9 +24,11 @@ public class PacketFCEncryptionResponse extends DefinedPacket
super( 0xFC ); super( 0xFC );
writeArray( sharedSecret ); writeArray( sharedSecret );
writeArray( verifyToken ); writeArray( verifyToken );
this.sharedSecret = sharedSecret;
this.verifyToken = verifyToken;
} }
public PacketFCEncryptionResponse(byte[] buf) PacketFCEncryptionResponse(ByteBuf buf)
{ {
super( 0xFC, buf ); super( 0xFC, buf );
this.sharedSecret = readArray(); this.sharedSecret = readArray();

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -15,7 +16,7 @@ public class PacketFDEncryptionRequest extends DefinedPacket
public PacketFDEncryptionRequest(String serverId, byte[] publicKey, byte[] verifyToken) public PacketFDEncryptionRequest(String serverId, byte[] publicKey, byte[] verifyToken)
{ {
super( 0xFD ); super( 0xFD );
writeUTF( serverId ); writeString( serverId );
writeArray( publicKey ); writeArray( publicKey );
writeArray( verifyToken ); writeArray( verifyToken );
this.serverId = serverId; this.serverId = serverId;
@ -23,10 +24,10 @@ public class PacketFDEncryptionRequest extends DefinedPacket
this.verifyToken = verifyToken; this.verifyToken = verifyToken;
} }
public PacketFDEncryptionRequest(byte[] buf) PacketFDEncryptionRequest(ByteBuf buf)
{ {
super( 0xFD, buf ); super( 0xFD, buf );
serverId = readUTF(); serverId = readString();
publicKey = readArray(); publicKey = readArray();
verifyToken = readArray(); verifyToken = readArray();
} }

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -8,9 +9,12 @@ import lombok.ToString;
public class PacketFEPing extends DefinedPacket public class PacketFEPing extends DefinedPacket
{ {
public PacketFEPing(byte[] buffer) public byte version;
PacketFEPing(ByteBuf buffer)
{ {
super( 0xFE, buffer ); super( 0xFE, buffer );
version = readByte();
} }
@Override @Override

View File

@ -1,5 +1,6 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -13,13 +14,13 @@ public class PacketFFKick extends DefinedPacket
public PacketFFKick(String message) public PacketFFKick(String message)
{ {
super( 0xFF ); super( 0xFF );
writeUTF( message ); writeString( message );
} }
public PacketFFKick(byte[] buf) PacketFFKick(ByteBuf buf)
{ {
super( 0xFF, buf ); super( 0xFF, buf );
this.message = readUTF(); this.message = readString();
} }
@Override @Override

View File

@ -1,70 +1,75 @@
package net.md_5.bungee.packet; package net.md_5.bungee.packet;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
public abstract class PacketHandler public abstract class PacketHandler
{ {
private void nop(DefinedPacket packet) @Override
public abstract String toString();
public void connected(Channel channel) throws Exception
{
}
public void disconnected(Channel channel) throws Exception
{
}
public void exception(Throwable t) throws Exception
{
}
public void handle(ByteBuf buf) throws Exception
{ {
throw new UnsupportedOperationException( "No handler defined for packet " + packet.getClass() );
} }
public void handle(Packet0KeepAlive alive) throws Exception public void handle(Packet0KeepAlive alive) throws Exception
{ {
nop( alive );
} }
public void handle(Packet1Login login) throws Exception public void handle(Packet1Login login) throws Exception
{ {
nop( login );
} }
public void handle(Packet2Handshake handshake) throws Exception public void handle(Packet2Handshake handshake) throws Exception
{ {
nop( handshake );
} }
public void handle(Packet3Chat chat) throws Exception public void handle(Packet3Chat chat) throws Exception
{ {
nop( chat );
} }
public void handle(Packet9Respawn respawn) throws Exception public void handle(Packet9Respawn respawn) throws Exception
{ {
nop( respawn );
} }
public void handle(PacketC9PlayerListItem playerList) throws Exception public void handle(PacketC9PlayerListItem playerList) throws Exception
{ {
nop( playerList );
} }
public void handle(PacketCDClientStatus clientStatus) throws Exception public void handle(PacketCDClientStatus clientStatus) throws Exception
{ {
nop( clientStatus );
} }
public void handle(PacketFAPluginMessage pluginMessage) throws Exception public void handle(PacketFAPluginMessage pluginMessage) throws Exception
{ {
nop( pluginMessage );
} }
public void handle(PacketFCEncryptionResponse encryptResponse) throws Exception public void handle(PacketFCEncryptionResponse encryptResponse) throws Exception
{ {
nop( encryptResponse );
} }
public void handle(PacketFDEncryptionRequest encryptRequest) throws Exception public void handle(PacketFDEncryptionRequest encryptRequest) throws Exception
{ {
nop( encryptRequest );
} }
public void handle(PacketFEPing ping) throws Exception public void handle(PacketFEPing ping) throws Exception
{ {
nop( ping );
} }
public void handle(PacketFFKick kick) throws Exception public void handle(PacketFFKick kick) throws Exception
{ {
nop( kick );
} }
} }

View File

@ -1,101 +0,0 @@
package net.md_5.bungee.packet;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import lombok.Getter;
import lombok.Setter;
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 PacketStream implements AutoCloseable
{
private final DataInputStream dataInput;
@Getter
private OutputStream out;
@Getter
@Setter
private int protocol;
private final TrackingInputStream tracker;
private final byte[] buffer = new byte[ 1 << 18 ];
public PacketStream(InputStream in, int protocol)
{
this( in, null, protocol );
}
public PacketStream(InputStream in, OutputStream out, int protocol)
{
tracker = new TrackingInputStream( in );
dataInput = new DataInputStream( tracker );
this.out = out;
this.protocol = protocol;
}
public void write(byte[] b) throws IOException
{
out.write( b );
}
public void write(DefinedPacket packet) throws IOException
{
out.write( packet.getPacket() );
}
/**
* 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, buffer, protocol );
return tracker.out.toByteArray();
}
@Override
public void close() throws Exception
{
dataInput.close();
}
/**
* Input stream which will wrap another stream and copy all bytes read to a
* {@link ByteArrayOutputStream}.
*/
private class TrackingInputStream extends FilterInputStream
{
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
public TrackingInputStream(InputStream in)
{
super( in );
}
@Override
public int read() throws IOException
{
int ret = in.read();
out.write( ret );
return ret;
}
@Override
public int read(byte[] b, int off, int len) throws IOException
{
int ret = in.read( b, off, len );
out.write( b, off, ret );
return ret;
}
}
}

View File

@ -22,7 +22,7 @@ public class GlobalTabList implements TabListHandler
UserConnection con = (UserConnection) player; UserConnection con = (UserConnection) player;
for ( ProxiedPlayer p : ProxyServer.getInstance().getPlayers() ) for ( ProxiedPlayer p : ProxyServer.getInstance().getPlayers() )
{ {
con.packetQueue.add( new PacketC9PlayerListItem( p.getDisplayName(), true, p.getPing() ) ); con.sendPacket(new PacketC9PlayerListItem( p.getDisplayName(), true, p.getPing() ) );
} }
BungeeCord.getInstance().broadcast( new PacketC9PlayerListItem( player.getDisplayName(), true, player.getPing() ) ); BungeeCord.getInstance().broadcast( new PacketC9PlayerListItem( player.getDisplayName(), true, player.getPing() ) );
} }

View File

@ -40,7 +40,7 @@ public class ServerUniqueTabList implements TabListHandler
{ {
for ( String username : usernames ) for ( String username : usernames )
{ {
( (UserConnection) player ).packetQueue.add( new PacketC9PlayerListItem( username, false, 9999 ) ); ( (UserConnection) player ).sendPacket(new PacketC9PlayerListItem( username, false, 9999 ) );
} }
usernames.clear(); usernames.clear();
} }