package net.md_5.bungee; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.util.internal.PlatformDependent; import java.net.InetSocketAddress; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.Locale; import java.util.Map; import java.util.Queue; import java.util.UUID; import java.util.logging.Level; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; import net.md_5.bungee.api.Callback; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.Title; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.PermissionCheckEvent; import net.md_5.bungee.api.event.ServerConnectEvent; import net.md_5.bungee.api.score.Scoreboard; import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.entitymap.EntityMap; import net.md_5.bungee.forge.ForgeClientHandler; import net.md_5.bungee.forge.ForgeConstants; import net.md_5.bungee.forge.ForgeServerHandler; import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.netty.HandlerBoss; import net.md_5.bungee.netty.PipelineUtils; import net.md_5.bungee.protocol.DefinedPacket; import net.md_5.bungee.protocol.MinecraftDecoder; import net.md_5.bungee.protocol.MinecraftEncoder; import net.md_5.bungee.protocol.PacketWrapper; import net.md_5.bungee.protocol.Protocol; import net.md_5.bungee.protocol.ProtocolConstants; import net.md_5.bungee.protocol.packet.Chat; import net.md_5.bungee.protocol.packet.ClientSettings; import net.md_5.bungee.protocol.packet.Kick; import net.md_5.bungee.protocol.packet.PlayerListHeaderFooter; import net.md_5.bungee.protocol.packet.PluginMessage; import net.md_5.bungee.protocol.packet.Respawn; import net.md_5.bungee.protocol.packet.SetCompression; import net.md_5.bungee.tab.ServerUnique; import net.md_5.bungee.tab.TabList; import net.md_5.bungee.util.CaseInsensitiveSet; @RequiredArgsConstructor public final class UserConnection implements ProxiedPlayer { /*========================================================================*/ @NonNull private final ProxyServer bungee; @NonNull private final ChannelWrapper ch; @Getter @NonNull private final String name; @Getter private final InitialHandler pendingConnection; /*========================================================================*/ @Getter @Setter private ServerConnection server; @Getter @Setter private int dimension; @Getter @Setter private boolean dimensionChange = true; @Getter private final Collection<ServerInfo> pendingConnects = new HashSet<>(); /*========================================================================*/ @Getter @Setter private int sentPingId; @Getter @Setter private long sentPingTime; @Getter @Setter private int ping = 100; @Getter @Setter private ServerInfo reconnectServer; @Getter private TabList tabListHandler; @Getter @Setter private int gamemode; @Getter private int compressionThreshold = -1; // Used for trying multiple servers in order @Setter private Queue<String> serverJoinQueue; /*========================================================================*/ private final Collection<String> groups = new CaseInsensitiveSet(); private final Collection<String> permissions = new CaseInsensitiveSet(); /*========================================================================*/ @Getter @Setter private int clientEntityId; @Getter @Setter private int serverEntityId; @Getter private ClientSettings settings; @Getter private final Scoreboard serverSentScoreboard = new Scoreboard(); @Getter private final Collection<UUID> sentBossBars = new HashSet<>(); /*========================================================================*/ @Getter private String displayName; @Getter private EntityMap entityRewrite; private Locale locale; /*========================================================================*/ @Getter @Setter private ForgeClientHandler forgeClientHandler; @Getter @Setter private ForgeServerHandler forgeServerHandler; /*========================================================================*/ private final Unsafe unsafe = new Unsafe() { @Override public void sendPacket(DefinedPacket packet) { ch.write( packet ); } }; public void init() { this.entityRewrite = EntityMap.getEntityMap( getPendingConnection().getVersion() ); this.displayName = name; /* switch ( getPendingConnection().getListener().getTabListType() ) { case "GLOBAL": tabListHandler = new Global( this ); break; case "SERVER": tabListHandler = new ServerUnique( this ); break; default: tabListHandler = new GlobalPing( this ); break; } */ tabListHandler = new ServerUnique( this ); Collection<String> g = bungee.getConfigurationAdapter().getGroups( name ); g.addAll( bungee.getConfigurationAdapter().getGroups( getUniqueId().toString() ) ); for ( String s : g ) { addGroups( s ); } forgeClientHandler = new ForgeClientHandler( this ); // Set whether the connection has a 1.8 FML marker in the handshake. forgeClientHandler.setFmlTokenInHandshake( this.getPendingConnection().getExtraDataInHandshake().contains( ForgeConstants.FML_HANDSHAKE_TOKEN ) ); } public void sendPacket(PacketWrapper packet) { ch.write( packet ); } @Deprecated public boolean isActive() { return !ch.isClosed(); } @Override public void setDisplayName(String name) { Preconditions.checkNotNull( name, "displayName" ); displayName = name; } @Override public void connect(ServerInfo target) { connect( target, null ); } @Override public void connect(ServerInfo target, Callback<Boolean> callback) { connect( target, callback, false ); } public void connectNow(ServerInfo target) { dimensionChange = true; connect( target ); } public ServerInfo updateAndGetNextServer(ServerInfo currentTarget) { if ( serverJoinQueue == null ) { serverJoinQueue = new LinkedList<>( getPendingConnection().getListener().getServerPriority() ); } ServerInfo next = null; while ( !serverJoinQueue.isEmpty() ) { ServerInfo candidate = ProxyServer.getInstance().getServerInfo( serverJoinQueue.remove() ); if ( !Objects.equal( currentTarget, candidate ) ) { next = candidate; break; } } return next; } public void connect(ServerInfo info, final Callback<Boolean> callback, final boolean retry) { Preconditions.checkNotNull( info, "info" ); ServerConnectEvent event = new ServerConnectEvent( this, info ); if ( bungee.getPluginManager().callEvent( event ).isCancelled() ) { if ( callback != null ) { callback.done( false, null ); } if ( getServer() == null && !ch.isClosing() ) { throw new IllegalStateException("Cancelled ServerConnectEvent with no server or disconnect."); } return; } final BungeeServerInfo target = (BungeeServerInfo) event.getTarget(); // Update in case the event changed target if ( getServer() != null && Objects.equal( getServer().getInfo(), target ) ) { if ( callback != null ) { callback.done( false, null ); } sendMessage( bungee.getTranslation( "already_connected" ) ); return; } if ( pendingConnects.contains( target ) ) { if ( callback != null ) { callback.done( false, null ); } sendMessage( bungee.getTranslation( "already_connecting" ) ); return; } pendingConnects.add( target ); ChannelInitializer initializer = new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { PipelineUtils.BASE.initChannel( ch ); ch.pipeline().addAfter( PipelineUtils.FRAME_DECODER, PipelineUtils.PACKET_DECODER, new MinecraftDecoder( Protocol.HANDSHAKE, false, getPendingConnection().getVersion() ) ); ch.pipeline().addAfter( PipelineUtils.FRAME_PREPENDER, PipelineUtils.PACKET_ENCODER, new MinecraftEncoder( Protocol.HANDSHAKE, false, getPendingConnection().getVersion() ) ); ch.pipeline().get( HandlerBoss.class ).setHandler( new ServerConnector( bungee, UserConnection.this, target ) ); } }; ChannelFutureListener listener = new ChannelFutureListener() { @Override @SuppressWarnings("ThrowableResultIgnored") public void operationComplete(ChannelFuture future) throws Exception { if ( callback != null ) { callback.done( future.isSuccess(), future.cause() ); } if ( !future.isSuccess() ) { future.channel().close(); pendingConnects.remove( target ); ServerInfo def = updateAndGetNextServer( target ); if ( retry && def != null && ( getServer() == null || def != getServer().getInfo() ) ) { sendMessage( bungee.getTranslation( "fallback_lobby" ) ); connect( def, null, true ); } else if ( dimensionChange ) { disconnect( bungee.getTranslation( "fallback_kick", future.cause().getClass().getName() ) ); } else { sendMessage( bungee.getTranslation( "fallback_kick", future.cause().getClass().getName() ) ); } } } }; Bootstrap b = new Bootstrap() .channel( PipelineUtils.getChannel() ) .group( ch.getHandle().eventLoop() ) .handler( initializer ) .option( ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000 ) // TODO: Configurable .remoteAddress( target.getAddress() ); // Windows is bugged, multi homed users will just have to live with random connecting IPs if ( getPendingConnection().getListener().isSetLocalAddress() && !PlatformDependent.isWindows() ) { b.localAddress( getPendingConnection().getListener().getHost().getHostString(), 0 ); } b.connect().addListener( listener ); } @Override public void disconnect(String reason) { disconnect0( TextComponent.fromLegacyText( reason ) ); } @Override public void disconnect(BaseComponent... reason) { disconnect0( reason ); } @Override public void disconnect(BaseComponent reason) { disconnect0( reason ); } public void disconnect0(final BaseComponent... reason) { if ( !ch.isClosing() ) { bungee.getLogger().log( Level.INFO, "[{0}] disconnected with: {1}", new Object[] { getName(), BaseComponent.toLegacyText( reason ) } ); ch.delayedClose( new Kick( ComponentSerializer.toString( reason ) ) ); if ( server != null ) { server.setObsolete( true ); server.disconnect( "Quitting" ); } } } @Override public void chat(String message) { Preconditions.checkState( server != null, "Not connected to server" ); server.getCh().write( new Chat( message ) ); } @Override public void sendMessage(String message) { sendMessage( TextComponent.fromLegacyText( message ) ); } @Override public void sendMessages(String... messages) { for ( String message : messages ) { sendMessage( message ); } } @Override public void sendMessage(BaseComponent... message) { sendMessage( ChatMessageType.CHAT, message ); } @Override public void sendMessage(BaseComponent message) { sendMessage( ChatMessageType.CHAT, message ); } private void sendMessage(ChatMessageType position, String message) { unsafe().sendPacket( new Chat( message, (byte) position.ordinal() ) ); } @Override public void sendMessage(ChatMessageType position, BaseComponent... message) { // Action bar on 1.8 doesn't display the new JSON formattings, legacy works - send it using this for now if ( position == ChatMessageType.ACTION_BAR && getPendingConnection().getVersion() <= ProtocolConstants.MINECRAFT_1_8 ) { sendMessage( position, ComponentSerializer.toString( new TextComponent( BaseComponent.toLegacyText( message ) ) ) ); } else { sendMessage( position, ComponentSerializer.toString( message ) ); } } @Override public void sendMessage(ChatMessageType position, BaseComponent message) { // Action bar on 1.8 doesn't display the new JSON formattings, legacy works - send it using this for now if ( position == ChatMessageType.ACTION_BAR && getPendingConnection().getVersion() <= ProtocolConstants.MINECRAFT_1_8 ) { sendMessage( position, ComponentSerializer.toString( new TextComponent( BaseComponent.toLegacyText( message ) ) ) ); } else { sendMessage( position, ComponentSerializer.toString( message ) ); } } @Override public void sendData(String channel, byte[] data) { unsafe().sendPacket( new PluginMessage( channel, data, forgeClientHandler.isForgeUser() ) ); } @Override public InetSocketAddress getAddress() { return (InetSocketAddress) ch.getHandle().remoteAddress(); } @Override public Collection<String> getGroups() { return Collections.unmodifiableCollection( groups ); } @Override public void addGroups(String... groups) { for ( String group : groups ) { this.groups.add( group ); for ( String permission : bungee.getConfigurationAdapter().getPermissions( group ) ) { setPermission( permission, true ); } } } @Override public void removeGroups(String... groups) { for ( String group : groups ) { this.groups.remove( group ); for ( String permission : bungee.getConfigurationAdapter().getPermissions( group ) ) { setPermission( permission, false ); } } } @Override public boolean hasPermission(String permission) { return bungee.getPluginManager().callEvent( new PermissionCheckEvent( this, permission, permissions.contains( permission ) ) ).hasPermission(); } @Override public void setPermission(String permission, boolean value) { if ( value ) { permissions.add( permission ); } else { permissions.remove( permission ); } } @Override public Collection<String> getPermissions() { return Collections.unmodifiableCollection( permissions ); } @Override public String toString() { return name; } @Override public Unsafe unsafe() { return unsafe; } @Override public String getUUID() { return getPendingConnection().getUUID(); } @Override public UUID getUniqueId() { return getPendingConnection().getUniqueId(); } public void setSettings(ClientSettings settings) { this.settings = settings; this.locale = null; } @Override public Locale getLocale() { return ( locale == null && settings != null ) ? locale = Locale.forLanguageTag( settings.getLocale().replaceAll( "_", "-" ) ) : locale; } @Override public boolean isForgeUser() { return forgeClientHandler.isForgeUser(); } @Override public Map<String, String> getModList() { if ( forgeClientHandler.getClientModList() == null ) { // Return an empty map, rather than a null, if the client hasn't got any mods, // or is yet to complete a handshake. return ImmutableMap.of(); } return ImmutableMap.copyOf( forgeClientHandler.getClientModList() ); } private static final String EMPTY_TEXT = ComponentSerializer.toString( new TextComponent( "" ) ); @Override public void setTabHeader(BaseComponent header, BaseComponent footer) { unsafe().sendPacket( new PlayerListHeaderFooter( ( header != null ) ? ComponentSerializer.toString( header ) : EMPTY_TEXT, ( footer != null ) ? ComponentSerializer.toString( footer ) : EMPTY_TEXT ) ); } @Override public void setTabHeader(BaseComponent[] header, BaseComponent[] footer) { unsafe().sendPacket( new PlayerListHeaderFooter( ( header != null ) ? ComponentSerializer.toString( header ) : EMPTY_TEXT, ( footer != null ) ? ComponentSerializer.toString( footer ) : EMPTY_TEXT ) ); } @Override public void resetTabHeader() { // Mojang did not add a way to remove the header / footer completely, we can only set it to empty setTabHeader( (BaseComponent) null, null ); } @Override public void sendTitle(Title title) { title.send( this ); } public String getExtraDataInHandshake() { return this.getPendingConnection().getExtraDataInHandshake(); } public void setCompressionThreshold(int compressionThreshold) { if ( !ch.isClosing() && this.compressionThreshold == -1 && compressionThreshold >= 0 ) { this.compressionThreshold = compressionThreshold; unsafe.sendPacket( new SetCompression( compressionThreshold ) ); ch.setCompressionThreshold( compressionThreshold ); } } @Override public boolean isConnected() { return !ch.isClosed(); } }