package net.md_5.bungee;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelException;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.util.ResourceLeakDetector;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import jline.console.ConsoleReader;
import lombok.Getter;
import lombok.Setter;
import lombok.Synchronized;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.Favicon;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.ReconnectHandler;
import net.md_5.bungee.api.ServerPing;
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.chat.TranslatableComponent;
import net.md_5.bungee.api.config.ConfigurationAdapter;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.plugin.PluginManager;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.chat.TextComponentSerializer;
import net.md_5.bungee.chat.TranslatableComponentSerializer;
import net.md_5.bungee.command.CommandBungee;
import net.md_5.bungee.command.CommandEnd;
import net.md_5.bungee.command.CommandIP;
import net.md_5.bungee.command.CommandPerms;
import net.md_5.bungee.command.CommandReload;
import net.md_5.bungee.command.ConsoleCommandSender;
import net.md_5.bungee.compress.CompressFactory;
import net.md_5.bungee.conf.Configuration;
import net.md_5.bungee.conf.YamlConfig;
import net.md_5.bungee.forge.ForgeConstants;
import net.md_5.bungee.log.BungeeLogger;
import net.md_5.bungee.log.LoggingOutputStream;
import net.md_5.bungee.module.ModuleManager;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.ProtocolConstants;
import net.md_5.bungee.protocol.packet.Chat;
import net.md_5.bungee.protocol.packet.PluginMessage;
import net.md_5.bungee.query.RemoteQuery;
import net.md_5.bungee.scheduler.BungeeScheduler;
import net.md_5.bungee.util.CaseInsensitiveMap;
import org.fusesource.jansi.AnsiConsole;
/**
* Main BungeeCord proxy class.
*/
public class BungeeCord extends ProxyServer
{
/**
* Current operation state.
*/
public volatile boolean isRunning;
/**
* Configuration.
*/
@Getter
public final Configuration config = new Configuration();
/**
* Localization bundle.
*/
private ResourceBundle baseBundle;
private ResourceBundle customBundle;
public EventLoopGroup eventLoops;
/**
* locations.yml save thread.
*/
private final Timer saveThread = new Timer( "Reconnect Saver" );
private final Timer metricsThread = new Timer( "Metrics Thread" );
/**
* Server socket listener.
*/
private final Collection<Channel> listeners = new HashSet<>();
/**
* Fully qualified connections.
*/
private final Map<String, UserConnection> connections = new CaseInsensitiveMap<>();
// Used to help with packet rewriting
private final Map<UUID, UserConnection> connectionsByOfflineUUID = new HashMap<>();
private final Map<UUID, UserConnection> connectionsByUUID = new HashMap<>();
private final ReadWriteLock connectionLock = new ReentrantReadWriteLock();
/**
* Plugin manager.
*/
@Getter
public final PluginManager pluginManager = new PluginManager( this );
@Getter
@Setter
private ReconnectHandler reconnectHandler;
@Getter
@Setter
private ConfigurationAdapter configurationAdapter = new YamlConfig();
private final Collection<String> pluginChannels = new HashSet<>();
@Getter
private final File pluginsFolder = new File( "plugins" );
@Getter
private final BungeeScheduler scheduler = new BungeeScheduler();
@Getter
private final ConsoleReader consoleReader;
@Getter
private final Logger logger;
public final Gson gson = new GsonBuilder()
.registerTypeAdapter( BaseComponent.class, new ComponentSerializer() )
.registerTypeAdapter( TextComponent.class, new TextComponentSerializer() )
.registerTypeAdapter( TranslatableComponent.class, new TranslatableComponentSerializer() )
.registerTypeAdapter( ServerPing.PlayerInfo.class, new PlayerInfoSerializer() )
.registerTypeAdapter( Favicon.class, Favicon.getFaviconTypeAdapter() ).create();
@Getter
private ConnectionThrottle connectionThrottle;
private final ModuleManager moduleManager = new ModuleManager();
{
// TODO: Proper fallback when we interface the manager
getPluginManager().registerCommand( null, new CommandReload() );
getPluginManager().registerCommand( null, new CommandEnd() );
getPluginManager().registerCommand( null, new CommandIP() );
getPluginManager().registerCommand( null, new CommandBungee() );
getPluginManager().registerCommand( null, new CommandPerms() );
registerChannel( "BungeeCord" );
}
public static BungeeCord getInstance()
{
return (BungeeCord) ProxyServer.getInstance();
}
@SuppressFBWarnings("DM_DEFAULT_ENCODING")
public BungeeCord() throws IOException
{
// Java uses ! to indicate a resource inside of a jar/zip/other container. Running Bungee from within a directory that has a ! will cause this to muck up.
Preconditions.checkState( new File( "." ).getAbsolutePath().indexOf( '!' ) == -1, "Cannot use BungeeCord in directory with ! in path." );
System.setSecurityManager( new BungeeSecurityManager() );
try
{
baseBundle = ResourceBundle.getBundle( "messages" );
} catch ( MissingResourceException ex )
{
baseBundle = ResourceBundle.getBundle( "messages", Locale.ENGLISH );
}
File file = new File( "messages.properties" );
if ( file.isFile() )
{
try ( FileReader rd = new FileReader( file ) )
{
customBundle = new PropertyResourceBundle( rd );
}
}
// This is a workaround for quite possibly the weirdest bug I have ever encountered in my life!
// When jansi attempts to extract its natives, by default it tries to extract a specific version,
// using the loading class's implementation version. Normally this works completely fine,
// however when on Windows certain characters such as - and : can trigger special behaviour.
// Furthermore this behaviour only occurs in specific combinations due to the parsing done by jansi.
// For example test-test works fine, but test-test-test does not! In order to avoid this all together but
// still keep our versions the same as they were, we set the override property to the essentially garbage version
// BungeeCord. This version is only used when extracting the libraries to their temp folder.
System.setProperty( "library.jansi.version", "BungeeCord" );
AnsiConsole.systemInstall();
consoleReader = new ConsoleReader();
consoleReader.setExpandEvents( false );
logger = new BungeeLogger( "BungeeCord", "proxy.log", consoleReader );
System.setErr( new PrintStream( new LoggingOutputStream( logger, Level.SEVERE ), true ) );
System.setOut( new PrintStream( new LoggingOutputStream( logger, Level.INFO ), true ) );
if ( !Boolean.getBoolean( "net.md_5.bungee.native.disable" ) )
{
if ( EncryptionUtil.nativeFactory.load() )
{
logger.info( "Using OpenSSL based native cipher." );
} else
{
logger.info( "Using standard Java JCE cipher. To enable the OpenSSL based native cipher, please make sure you are using 64 bit Ubuntu or Debian with libssl installed." );
}
if ( CompressFactory.zlib.load() )
{
logger.info( "Using native code compressor" );
} else
{
logger.info( "Using standard Java compressor. To enable zero copy compression, run on 64 bit Linux" );
}
}
}
/**
* Start this proxy instance by loading the configuration, plugins and
* starting the connect thread.
*
* @throws Exception
*/
@Override
@SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
public void start() throws Exception
{
System.setProperty( "java.net.preferIPv4Stack", "true" ); // Minecraft does not support IPv6
System.setProperty( "io.netty.selectorAutoRebuildThreshold", "0" ); // Seems to cause Bungee to stop accepting connections
if ( System.getProperty( "io.netty.leakDetectionLevel" ) == null )
{
ResourceLeakDetector.setLevel( ResourceLeakDetector.Level.DISABLED ); // Eats performance
}
eventLoops = PipelineUtils.newEventLoopGroup( 0, new ThreadFactoryBuilder().setNameFormat( "Netty IO Thread #%1$d" ).build() );
File moduleDirectory = new File( "modules" );
moduleManager.load( this, moduleDirectory );
pluginManager.detectPlugins( moduleDirectory );
pluginsFolder.mkdir();
pluginManager.detectPlugins( pluginsFolder );
pluginManager.loadPlugins();
config.load();
registerChannel( ForgeConstants.FML_TAG );
registerChannel( ForgeConstants.FML_HANDSHAKE_TAG );
registerChannel( ForgeConstants.FORGE_REGISTER );
isRunning = true;
pluginManager.enablePlugins();
if ( config.getThrottle() > 0 )
{
connectionThrottle = new ConnectionThrottle( config.getThrottle() );
}
startListeners();
saveThread.scheduleAtFixedRate( new TimerTask()
{
@Override
public void run()
{
if ( getReconnectHandler() != null )
{
getReconnectHandler().save();
}
}
}, 0, TimeUnit.MINUTES.toMillis( 5 ) );
metricsThread.scheduleAtFixedRate( new Metrics(), 0, TimeUnit.MINUTES.toMillis( Metrics.PING_INTERVAL ) );
}
public void startListeners()
{
for ( final ListenerInfo info : config.getListeners() )
{
ChannelFutureListener listener = new ChannelFutureListener()
{
@Override
public void operationComplete(ChannelFuture future) throws Exception
{
if ( future.isSuccess() )
{
listeners.add( future.channel() );
getLogger().log( Level.INFO, "Listening on {0}", info.getHost() );
} else
{
getLogger().log( Level.WARNING, "Could not bind to host " + info.getHost(), future.cause() );
}
}
};
new ServerBootstrap()
.channel( PipelineUtils.getServerChannel() )
.option( ChannelOption.SO_REUSEADDR, true ) // TODO: Move this elsewhere!
.childAttr( PipelineUtils.LISTENER, info )
.childHandler( PipelineUtils.SERVER_CHILD )
.group( eventLoops )
.localAddress( info.getHost() )
.bind().addListener( listener );
if ( info.isQueryEnabled() )
{
ChannelFutureListener bindListener = new ChannelFutureListener()
{
@Override
public void operationComplete(ChannelFuture future) throws Exception
{
if ( future.isSuccess() )
{
listeners.add( future.channel() );
getLogger().log( Level.INFO, "Started query on {0}", future.channel().localAddress() );
} else
{
getLogger().log( Level.WARNING, "Could not bind to host " + info.getHost(), future.cause() );
}
}
};
new RemoteQuery( this, info ).start( PipelineUtils.getDatagramChannel(), new InetSocketAddress( info.getHost().getAddress(), info.getQueryPort() ), eventLoops, bindListener );
}
}
}
public void stopListeners()
{
for ( Channel listener : listeners )
{
getLogger().log( Level.INFO, "Closing listener {0}", listener );
try
{
listener.close().syncUninterruptibly();
} catch ( ChannelException ex )
{
getLogger().severe( "Could not close listen thread" );
}
}
listeners.clear();
}
@Override
public void stop()
{
stop( getTranslation( "restart" ) );
}
@Override
public void stop(final String reason)
{
new Thread( "Shutdown Thread" )
{
@Override
@SuppressFBWarnings("DM_EXIT")
@SuppressWarnings("TooBroadCatch")
public void run()
{
BungeeCord.this.isRunning = false;
stopListeners();
getLogger().info( "Closing pending connections" );
connectionLock.readLock().lock();
try
{
getLogger().log( Level.INFO, "Disconnecting {0} connections", connections.size() );
for ( UserConnection user : connections.values() )
{
user.disconnect( reason );
}
} finally
{
connectionLock.readLock().unlock();
}
try
{
Thread.sleep( 500 );
} catch ( InterruptedException ex )
{
}
getLogger().info( "Closing IO threads" );
eventLoops.shutdownGracefully();
try
{
eventLoops.awaitTermination( Long.MAX_VALUE, TimeUnit.NANOSECONDS );
} catch ( InterruptedException ex )
{
}
if ( reconnectHandler != null )
{
getLogger().info( "Saving reconnect locations" );
reconnectHandler.save();
reconnectHandler.close();
}
saveThread.cancel();
metricsThread.cancel();
// TODO: Fix this shit
getLogger().info( "Disabling plugins" );
for ( Plugin plugin : Lists.reverse( new ArrayList<>( pluginManager.getPlugins() ) ) )
{
try
{
plugin.onDisable();
for ( Handler handler : plugin.getLogger().getHandlers() )
{
handler.close();
}
} catch ( Throwable t )
{
getLogger().log( Level.SEVERE, "Exception disabling plugin " + plugin.getDescription().getName(), t );
}
getScheduler().cancel( plugin );
plugin.getExecutorService().shutdownNow();
}
getLogger().info( "Thank you and goodbye" );
// Need to close loggers after last message!
for ( Handler handler : getLogger().getHandlers() )
{
handler.close();
}
System.exit( 0 );
}
}.start();
}
/**
* Broadcasts a packet to all clients that is connected to this instance.
*
* @param packet the packet to send
*/
public void broadcast(DefinedPacket packet)
{
connectionLock.readLock().lock();
try
{
for ( UserConnection con : connections.values() )
{
con.unsafe().sendPacket( packet );
}
} finally
{
connectionLock.readLock().unlock();
}
}
@Override
public String getName()
{
return "BungeeCord";
}
@Override
public String getVersion()
{
return ( BungeeCord.class.getPackage().getImplementationVersion() == null ) ? "unknown" : BungeeCord.class.getPackage().getImplementationVersion();
}
@Override
public String getTranslation(String name, Object... args)
{
String translation = "<translation '" + name + "' missing>";
try
{
translation = MessageFormat.format( customBundle != null && customBundle.containsKey( name ) ? customBundle.getString( name ) : baseBundle.getString( name ), args );
} catch ( MissingResourceException ex )
{
}
return translation;
}
@Override
@SuppressWarnings("unchecked")
public Collection<ProxiedPlayer> getPlayers()
{
connectionLock.readLock().lock();
try
{
return Collections.unmodifiableCollection( new HashSet( connections.values() ) );
} finally
{
connectionLock.readLock().unlock();
}
}
@Override
public int getOnlineCount()
{
return connections.size();
}
@Override
public ProxiedPlayer getPlayer(String name)
{
connectionLock.readLock().lock();
try
{
return connections.get( name );
} finally
{
connectionLock.readLock().unlock();
}
}
public UserConnection getPlayerByOfflineUUID(UUID name)
{
connectionLock.readLock().lock();
try
{
return connectionsByOfflineUUID.get( name );
} finally
{
connectionLock.readLock().unlock();
}
}
@Override
public ProxiedPlayer getPlayer(UUID uuid)
{
connectionLock.readLock().lock();
try
{
return connectionsByUUID.get( uuid );
} finally
{
connectionLock.readLock().unlock();
}
}
@Override
public Map<String, ServerInfo> getServers()
{
return config.getServers();
}
@Override
public ServerInfo getServerInfo(String name)
{
return getServers().get( name );
}
@Override
@Synchronized("pluginChannels")
public void registerChannel(String channel)
{
pluginChannels.add( channel );
}
@Override
@Synchronized("pluginChannels")
public void unregisterChannel(String channel)
{
pluginChannels.remove( channel );
}
@Override
@Synchronized("pluginChannels")
public Collection<String> getChannels()
{
return Collections.unmodifiableCollection( pluginChannels );
}
public PluginMessage registerChannels()
{
return new PluginMessage( "REGISTER", Util.format( pluginChannels, "\00" ).getBytes( Charsets.UTF_8 ), false );
}
@Override
public int getProtocolVersion()
{
return ProtocolConstants.SUPPORTED_VERSION_IDS.get( ProtocolConstants.SUPPORTED_VERSION_IDS.size() - 1 );
}
@Override
public String getGameVersion()
{
return Joiner.on( ", " ).join( ProtocolConstants.SUPPORTED_VERSIONS );
}
@Override
public ServerInfo constructServerInfo(String name, InetSocketAddress address, String motd, boolean restricted)
{
return new BungeeServerInfo( name, address, motd, restricted );
}
@Override
public CommandSender getConsole()
{
return ConsoleCommandSender.getInstance();
}
@Override
public void broadcast(String message)
{
broadcast( TextComponent.fromLegacyText( message ) );
}
@Override
public void broadcast(BaseComponent... message)
{
getConsole().sendMessage( BaseComponent.toLegacyText( message ) );
broadcast( new Chat( ComponentSerializer.toString( message ) ) );
}
@Override
public void broadcast(BaseComponent message)
{
getConsole().sendMessage( message.toLegacyText() );
broadcast( new Chat( ComponentSerializer.toString( message ) ) );
}
public void addConnection(UserConnection con)
{
connectionLock.writeLock().lock();
try
{
connections.put( con.getName(), con );
connectionsByUUID.put( con.getUniqueId(), con );
connectionsByOfflineUUID.put( con.getPendingConnection().getOfflineId(), con );
} finally
{
connectionLock.writeLock().unlock();
}
}
public void removeConnection(UserConnection con)
{
connectionLock.writeLock().lock();
try
{
// TODO See #1218
if ( connections.get( con.getName() ) == con )
{
connections.remove( con.getName() );
connectionsByUUID.remove( con.getUniqueId() );
connectionsByOfflineUUID.remove( con.getPendingConnection().getOfflineId() );
}
} finally
{
connectionLock.writeLock().unlock();
}
}
@Override
public Collection<String> getDisabledCommands()
{
return config.getDisabledCommands();
}
@Override
public Collection<ProxiedPlayer> matchPlayer(final String partialName)
{
Preconditions.checkNotNull( partialName, "partialName" );
ProxiedPlayer exactMatch = getPlayer( partialName );
if ( exactMatch != null )
{
return Collections.singleton( exactMatch );
}
return Sets.newHashSet( Iterables.filter( getPlayers(), new Predicate<ProxiedPlayer>()
{
@Override
public boolean apply(ProxiedPlayer input)
{
return ( input == null ) ? false : input.getName().toLowerCase().startsWith( partialName.toLowerCase() );
}
} ) );
}
@Override
public Title createTitle()
{
return new BungeeTitle();
}
}