package net.md_5.bungee.connection;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.crypto.SecretKey;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.BungeeServerInfo;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.Util;
import net.md_5.bungee.api.AbstractReconnectHandler;
import net.md_5.bungee.api.Callback;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.Favicon;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.Connection.Unsafe;
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.PlayerHandshakeEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.event.ProxyPingEvent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.http.HttpClient;
import net.md_5.bungee.jni.cipher.BungeeCipher;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketHandler;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.netty.cipher.CipherDecoder;
import net.md_5.bungee.netty.cipher.CipherEncoder;
import net.md_5.bungee.protocol.DefinedPacket;
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.EncryptionRequest;
import net.md_5.bungee.protocol.packet.EncryptionResponse;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LegacyHandshake;
import net.md_5.bungee.protocol.packet.LegacyPing;
import net.md_5.bungee.protocol.packet.LoginRequest;
import net.md_5.bungee.protocol.packet.LoginSuccess;
import net.md_5.bungee.protocol.packet.PingPacket;
import net.md_5.bungee.protocol.packet.PluginMessage;
import net.md_5.bungee.protocol.packet.StatusRequest;
import net.md_5.bungee.protocol.packet.StatusResponse;
import net.md_5.bungee.util.BoundedArrayList;
@RequiredArgsConstructor
public class InitialHandler extends PacketHandler implements PendingConnection
{
private final BungeeCord bungee;
private ChannelWrapper ch;
@Getter
private final ListenerInfo listener;
@Getter
private Handshake handshake;
@Getter
private LoginRequest loginRequest;
private EncryptionRequest request;
@Getter
private final List<PluginMessage> relayMessages = new BoundedArrayList<>( 128 );
private State thisState = State.HANDSHAKE;
private final Unsafe unsafe = new Unsafe()
{
@Override
public void sendPacket(DefinedPacket packet)
{
ch.write( packet );
}
};
@Getter
private boolean onlineMode = BungeeCord.getInstance().config.isOnlineMode();
@Getter
private InetSocketAddress virtualHost;
private String name;
@Getter
private UUID uniqueId;
@Getter
private UUID offlineId;
@Getter
private LoginResult loginProfile;
@Getter
private boolean legacy;
@Getter
private String extraDataInHandshake = "";
@Override
public boolean shouldHandle(PacketWrapper packet) throws Exception
{
return !ch.isClosing();
}
private enum State
{
HANDSHAKE, STATUS, PING, USERNAME, ENCRYPT, FINISHED;
}
@Override
public void connected(ChannelWrapper channel) throws Exception
{
this.ch = channel;
}
@Override
public void exception(Throwable t) throws Exception
{
disconnect( ChatColor.RED + Util.exception( t ) );
}
@Override
public void handle(PluginMessage pluginMessage) throws Exception
{
// TODO: Unregister?
if ( PluginMessage.SHOULD_RELAY.apply( pluginMessage ) )
{
relayMessages.add( pluginMessage );
}
}
@Override
public void handle(LegacyHandshake legacyHandshake) throws Exception
{
this.legacy = true;
ch.close( bungee.getTranslation( "outdated_client" ) );
}
@Override
public void handle(LegacyPing ping) throws Exception
{
this.legacy = true;
final boolean v1_5 = ping.isV1_5();
ServerPing legacy = new ServerPing( new ServerPing.Protocol( bungee.getName() + " " + bungee.getGameVersion(), bungee.getProtocolVersion() ),
new ServerPing.Players( listener.getMaxPlayers(), bungee.getOnlineCount(), null ),
new TextComponent( TextComponent.fromLegacyText( listener.getMotd() ) ), (Favicon) null );
Callback<ProxyPingEvent> callback = new Callback<ProxyPingEvent>()
{
@Override
public void done(ProxyPingEvent result, Throwable error)
{
if ( ch.isClosed() )
{
return;
}
ServerPing legacy = result.getResponse();
String kickMessage;
if ( v1_5 )
{
kickMessage = ChatColor.DARK_BLUE
+ "\00" + 127
+ '\00' + legacy.getVersion().getName()
+ '\00' + getFirstLine( legacy.getDescription() )
+ '\00' + legacy.getPlayers().getOnline()
+ '\00' + legacy.getPlayers().getMax();
} else
{
// Clients <= 1.3 don't support colored motds because the color char is used as delimiter
kickMessage = ChatColor.stripColor( getFirstLine( legacy.getDescription() ) )
+ '\u00a7' + legacy.getPlayers().getOnline()
+ '\u00a7' + legacy.getPlayers().getMax();
}
ch.getHandle().writeAndFlush( kickMessage );
ch.close();
}
};
bungee.getPluginManager().callEvent( new ProxyPingEvent( this, legacy, callback ) );
}
private static String getFirstLine(String str)
{
int pos = str.indexOf( '\n' );
return pos == -1 ? str : str.substring( 0, pos );
}
@Override
public void handle(StatusRequest statusRequest) throws Exception
{
Preconditions.checkState( thisState == State.STATUS, "Not expecting STATUS" );
ServerInfo forced = AbstractReconnectHandler.getForcedHost( this );
final String motd = ( forced != null ) ? forced.getMotd() : listener.getMotd();
Callback<ServerPing> pingBack = new Callback<ServerPing>()
{
@Override
public void done(ServerPing result, Throwable error)
{
if ( error != null )
{
result = new ServerPing();
result.setDescription( bungee.getTranslation( "ping_cannot_connect" ) );
bungee.getLogger().log( Level.WARNING, "Error pinging remote server", error );
}
Callback<ProxyPingEvent> callback = new Callback<ProxyPingEvent>()
{
@Override
public void done(ProxyPingEvent pingResult, Throwable error)
{
Gson gson = BungeeCord.getInstance().gson;
unsafe.sendPacket( new StatusResponse( gson.toJson( pingResult.getResponse() ) ) );
}
};
bungee.getPluginManager().callEvent( new ProxyPingEvent( InitialHandler.this, result, callback ) );
}
};
if ( forced != null && listener.isPingPassthrough() )
{
( (BungeeServerInfo) forced ).ping( pingBack, handshake.getProtocolVersion() );
} else
{
int protocol = ( ProtocolConstants.SUPPORTED_VERSION_IDS.contains( handshake.getProtocolVersion() ) ) ? handshake.getProtocolVersion() : bungee.getProtocolVersion();
pingBack.done( new ServerPing(
new ServerPing.Protocol( bungee.getName() + " " + bungee.getGameVersion(), protocol ),
new ServerPing.Players( listener.getMaxPlayers(), bungee.getOnlineCount(), null ),
motd, BungeeCord.getInstance().config.getFaviconObject() ),
null );
}
thisState = State.PING;
}
@Override
public void handle(PingPacket ping) throws Exception
{
Preconditions.checkState( thisState == State.PING, "Not expecting PING" );
unsafe.sendPacket( ping );
disconnect( "" );
}
@Override
public void handle(Handshake handshake) throws Exception
{
Preconditions.checkState( thisState == State.HANDSHAKE, "Not expecting HANDSHAKE" );
this.handshake = handshake;
ch.setVersion( handshake.getProtocolVersion() );
// Starting with FML 1.8, a "\0FML\0" token is appended to the handshake. This interferes
// with Bungee's IP forwarding, so we detect it, and remove it from the host string, for now.
// We know FML appends \00FML\00. However, we need to also consider that other systems might
// add their own data to the end of the string. So, we just take everything from the \0 character
// and save it for later.
if ( handshake.getHost().contains( "\0" ) )
{
String[] split = handshake.getHost().split( "\0", 2 );
handshake.setHost( split[0] );
extraDataInHandshake = "\0" + split[1];
}
// SRV records can end with a . depending on DNS / client.
if ( handshake.getHost().endsWith( "." ) )
{
handshake.setHost( handshake.getHost().substring( 0, handshake.getHost().length() - 1 ) );
}
this.virtualHost = InetSocketAddress.createUnresolved( handshake.getHost(), handshake.getPort() );
bungee.getLogger().log( Level.INFO, "{0} has connected", this );
bungee.getPluginManager().callEvent( new PlayerHandshakeEvent( InitialHandler.this, handshake ) );
switch ( handshake.getRequestedProtocol() )
{
case 1:
// Ping
thisState = State.STATUS;
ch.setProtocol( Protocol.STATUS );
break;
case 2:
// Login
thisState = State.USERNAME;
ch.setProtocol( Protocol.LOGIN );
if ( !ProtocolConstants.SUPPORTED_VERSION_IDS.contains( handshake.getProtocolVersion() ) )
{
if ( handshake.getProtocolVersion() > bungee.getProtocolVersion() )
{
disconnect( bungee.getTranslation( "outdated_server" ) );
} else
{
disconnect( bungee.getTranslation( "outdated_client" ) );
}
return;
}
if ( bungee.getConnectionThrottle() != null && bungee.getConnectionThrottle().throttle( getAddress().getAddress() ) )
{
disconnect( bungee.getTranslation( "join_throttle_kick", TimeUnit.MILLISECONDS.toSeconds( bungee.getConfig().getThrottle() ) ) );
}
break;
default:
throw new IllegalArgumentException( "Cannot request protocol " + handshake.getRequestedProtocol() );
}
}
@Override
public void handle(LoginRequest loginRequest) throws Exception
{
Preconditions.checkState( thisState == State.USERNAME, "Not expecting USERNAME" );
this.loginRequest = loginRequest;
if ( getName().contains( "." ) )
{
disconnect( bungee.getTranslation( "name_invalid" ) );
return;
}
if ( getName().length() > 16 )
{
disconnect( bungee.getTranslation( "name_too_long" ) );
return;
}
int limit = BungeeCord.getInstance().config.getPlayerLimit();
if ( limit > 0 && bungee.getOnlineCount() > limit )
{
disconnect( bungee.getTranslation( "proxy_full" ) );
return;
}
// If offline mode and they are already on, don't allow connect
// We can just check by UUID here as names are based on UUID
if ( !isOnlineMode() && bungee.getPlayer( getUniqueId() ) != null )
{
disconnect( bungee.getTranslation( "already_connected_proxy" ) );
return;
}
Callback<PreLoginEvent> callback = new Callback<PreLoginEvent>()
{
@Override
public void done(PreLoginEvent result, Throwable error)
{
if ( result.isCancelled() )
{
disconnect( result.getCancelReasonComponents() );
return;
}
if ( ch.isClosed() )
{
return;
}
if ( onlineMode )
{
unsafe().sendPacket( request = EncryptionUtil.encryptRequest() );
} else
{
finish();
}
thisState = State.ENCRYPT;
}
};
// fire pre login event
bungee.getPluginManager().callEvent( new PreLoginEvent( InitialHandler.this, callback ) );
}
@Override
public void handle(final EncryptionResponse encryptResponse) throws Exception
{
Preconditions.checkState( thisState == State.ENCRYPT, "Not expecting ENCRYPT" );
SecretKey sharedKey = EncryptionUtil.getSecret( encryptResponse, request );
BungeeCipher decrypt = EncryptionUtil.getCipher( false, sharedKey );
ch.addBefore( PipelineUtils.FRAME_DECODER, PipelineUtils.DECRYPT_HANDLER, new CipherDecoder( decrypt ) );
BungeeCipher encrypt = EncryptionUtil.getCipher( true, sharedKey );
ch.addBefore( PipelineUtils.FRAME_PREPENDER, PipelineUtils.ENCRYPT_HANDLER, new CipherEncoder( encrypt ) );
String encName = URLEncoder.encode( InitialHandler.this.getName(), "UTF-8" );
MessageDigest sha = MessageDigest.getInstance( "SHA-1" );
for ( byte[] bit : new byte[][]
{
request.getServerId().getBytes( "ISO_8859_1" ), sharedKey.getEncoded(), EncryptionUtil.keys.getPublic().getEncoded()
} )
{
sha.update( bit );
}
String encodedHash = URLEncoder.encode( new BigInteger( sha.digest() ).toString( 16 ), "UTF-8" );
String preventProxy = ( ( BungeeCord.getInstance().config.isPreventProxyConnections() ) ? "&ip=" + URLEncoder.encode( getAddress().getAddress().getHostAddress(), "UTF-8" ) : "" );
String authURL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + encName + "&serverId=" + encodedHash + preventProxy;
Callback<String> handler = new Callback<String>()
{
@Override
public void done(String result, Throwable error)
{
if ( error == null )
{
LoginResult obj = BungeeCord.getInstance().gson.fromJson( result, LoginResult.class );
if ( obj != null && obj.getId() != null )
{
loginProfile = obj;
name = obj.getName();
uniqueId = Util.getUUID( obj.getId() );
finish();
return;
}
disconnect( bungee.getTranslation( "offline_mode_player" ) );
} else
{
disconnect( bungee.getTranslation( "mojang_fail" ) );
bungee.getLogger().log( Level.SEVERE, "Error authenticating " + getName() + " with minecraft.net", error );
}
}
};
HttpClient.get( authURL, ch.getHandle().eventLoop(), handler );
}
private void finish()
{
if ( isOnlineMode() )
{
// Check for multiple connections
// We have to check for the old name first
ProxiedPlayer oldName = bungee.getPlayer( getName() );
if ( oldName != null )
{
// TODO See #1218
oldName.disconnect( bungee.getTranslation( "already_connected_proxy" ) );
}
// And then also for their old UUID
ProxiedPlayer oldID = bungee.getPlayer( getUniqueId() );
if ( oldID != null )
{
// TODO See #1218
oldID.disconnect( bungee.getTranslation( "already_connected_proxy" ) );
}
} else
{
// In offline mode the existing user stays and we kick the new one
ProxiedPlayer oldName = bungee.getPlayer( getName() );
if ( oldName != null )
{
// TODO See #1218
disconnect( bungee.getTranslation( "already_connected_proxy" ) );
return;
}
}
offlineId = java.util.UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + getName() ).getBytes( Charsets.UTF_8 ) );
if ( uniqueId == null )
{
uniqueId = offlineId;
}
Callback<LoginEvent> complete = new Callback<LoginEvent>()
{
@Override
public void done(LoginEvent result, Throwable error)
{
if ( result.isCancelled() )
{
disconnect( result.getCancelReasonComponents() );
return;
}
if ( ch.isClosed() )
{
return;
}
ch.getHandle().eventLoop().execute( new Runnable()
{
@Override
public void run()
{
if ( !ch.isClosing() )
{
UserConnection userCon = new UserConnection( bungee, ch, getName(), InitialHandler.this );
userCon.setCompressionThreshold( BungeeCord.getInstance().config.getCompressionThreshold() );
userCon.init();
unsafe.sendPacket( new LoginSuccess( getUniqueId().toString(), getName() ) ); // With dashes in between
ch.setProtocol( Protocol.GAME );
ch.getHandle().pipeline().get( HandlerBoss.class ).setHandler( new UpstreamBridge( bungee, userCon ) );
bungee.getPluginManager().callEvent( new PostLoginEvent( userCon ) );
ServerInfo server;
if ( bungee.getReconnectHandler() != null )
{
server = bungee.getReconnectHandler().getServer( userCon );
} else
{
server = AbstractReconnectHandler.getForcedHost( InitialHandler.this );
}
if ( server == null )
{
server = bungee.getServerInfo( listener.getDefaultServer() );
}
userCon.connect( server, null, true );
thisState = State.FINISHED;
}
}
} );
}
};
// fire login event
bungee.getPluginManager().callEvent( new LoginEvent( InitialHandler.this, complete ) );
}
@Override
public void disconnect(String reason)
{
disconnect( TextComponent.fromLegacyText( reason ) );
}
@Override
public void disconnect(final BaseComponent... reason)
{
if ( thisState != State.STATUS && thisState != State.PING )
{
ch.delayedClose( new Kick( ComponentSerializer.toString( reason ) ) );
} else
{
ch.close();
}
}
@Override
public void disconnect(BaseComponent reason)
{
disconnect( new BaseComponent[]
{
reason
} );
}
@Override
public String getName()
{
return (name != null ) ? name : ( loginRequest == null ) ? null : loginRequest.getData();
}
@Override
public int getVersion()
{
return ( handshake == null ) ? -1 : handshake.getProtocolVersion();
}
@Override
public InetSocketAddress getAddress()
{
return (InetSocketAddress) ch.getHandle().remoteAddress();
}
@Override
public Unsafe unsafe()
{
return unsafe;
}
@Override
public void setOnlineMode(boolean onlineMode)
{
Preconditions.checkState( thisState == State.USERNAME, "Can only set online mode status whilst state is username" );
this.onlineMode = onlineMode;
}
@Override
public void setUniqueId(UUID uuid)
{
Preconditions.checkState( thisState == State.USERNAME, "Can only set uuid while state is username" );
Preconditions.checkState( !onlineMode, "Can only set uuid when online mode is false" );
this.uniqueId = uuid;
}
@Override
public String getUUID()
{
return uniqueId.toString().replaceAll( "-", "" );
}
@Override
public String toString()
{
return "[" + ( ( getName() != null ) ? getName() : getAddress() ) + "] <-> InitialHandler";
}
@Override
public boolean isConnected()
{
return !ch.isClosed();
}
}