diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java index ce084d0a..c8d1211f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -47,6 +47,8 @@ class ProtocolConfig { private static final String UPDATER_DELAY = "delay"; private static final String UPDATER_LAST_TIME = "last"; + private static final String ALTERNATIVE_JVM = "alternate jvm"; + // Defaults private static final long DEFAULT_UPDATER_DELAY = 43200; @@ -229,6 +231,22 @@ class ProtocolConfig { global.set(BACKGROUND_COMPILER_ENABLED, enabled); } + /** + * Retrieve whether the current JVM is a non-standard implementation and require some workarounds. + * @return TRUE if it does, FALSE otherwise. + */ + public boolean isAlternateJVM() { + return global.getBoolean(ALTERNATIVE_JVM, false); + } + + /** + * Set whether the current JVM is a non-standard implementation and require some workarounds. + * @param value - TRUE if it is, FALSE otherwise. + */ + public void setAlternateJVM(boolean value) { + global.set(ALTERNATIVE_JVM, value); + } + /** * Set the last time we updated, in seconds since 1970.01.01 00:00. * @param lastTimeSeconds - new last update time. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 0804fe51..07497651 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -136,7 +136,10 @@ public class ProtocolLibrary extends JavaPlugin { updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info"); unhookTask = new DelayedSingleTask(this); - protocolManager = new PacketFilterManager(getClassLoader(), getServer(), unhookTask, detailedReporter); + protocolManager = new PacketFilterManager( + getClassLoader(), getServer(), unhookTask, detailedReporter, config.isAlternateJVM()); + + // Setup error reporter detailedReporter.addGlobalParameter("manager", protocolManager); // Update injection hook diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java index e373126d..ca58b656 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -150,7 +150,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok * Only create instances of this class if protocol lib is disabled. * @param unhookTask */ - public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, ErrorReporter reporter) { + public PacketFilterManager(ClassLoader classLoader, Server server, DelayedSingleTask unhookTask, ErrorReporter reporter, boolean alternateJVM) { if (reporter == null) throw new IllegalArgumentException("reporter cannot be NULL."); if (classLoader == null) @@ -200,6 +200,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok classLoader(classLoader). packetListeners(packetListeners). injectionFilter(isInjectionNecessary). + alternativeJVM(alternateJVM). buildHandler(); this.packetInjector = PacketInjectorBuilder.newBuilder(). diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java index 9b87c532..4dd2fb9a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java @@ -27,7 +27,7 @@ import net.sf.cglib.proxy.Factory; import org.bukkit.Server; import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.injector.server.InputStreamPlayerLookup; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.ObjectWriter; @@ -55,7 +55,7 @@ class InjectedServerConnection { private NetLoginInjector netLoginInjector; // Inject server connections - private InputStreamPlayerLookup socketInjector; + private AbstractInputStreamLookup socketInjector; private Server server; private ErrorReporter reporter; @@ -64,7 +64,7 @@ class InjectedServerConnection { private Object minecraftServer = null; - public InjectedServerConnection(ErrorReporter reporter, InputStreamPlayerLookup socketInjector, Server server, NetLoginInjector netLoginInjector) { + public InjectedServerConnection(ErrorReporter reporter, AbstractInputStreamLookup socketInjector, Server server, NetLoginInjector netLoginInjector) { this.listFields = new ArrayList(); this.replacedLists = new ArrayList>(); this.reporter = reporter; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java index 4a4b806f..cb2032e6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java @@ -25,7 +25,7 @@ import org.bukkit.entity.Player; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.injector.GamePhase; -import com.comphenix.protocol.injector.server.InputStreamPlayerLookup; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; import com.comphenix.protocol.injector.server.SocketInjector; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.utility.MinecraftReflection; @@ -45,15 +45,15 @@ class NetLoginInjector { private ProxyPlayerInjectionHandler injectionHandler; // Associate input streams and injectors - private InputStreamPlayerLookup serverSocket; + private AbstractInputStreamLookup inputStreamLookup; // The current error rerporter private ErrorReporter reporter; - public NetLoginInjector(ErrorReporter reporter, ProxyPlayerInjectionHandler injectionHandler, InputStreamPlayerLookup serverSocket) { + public NetLoginInjector(ErrorReporter reporter, ProxyPlayerInjectionHandler injectionHandler, AbstractInputStreamLookup inputStreamLookup) { this.reporter = reporter; this.injectionHandler = injectionHandler; - this.serverSocket = serverSocket; + this.inputStreamLookup = inputStreamLookup; } /** @@ -74,7 +74,7 @@ class NetLoginInjector { // Get the underlying socket Socket socket = (Socket) getSocketMethod.invoke(inserting); - SocketInjector socketInjector = serverSocket.getSocketInjector(socket); + SocketInjector socketInjector = inputStreamLookup.getSocketInjector(socket); // This is the case if we're dealing with a connection initiated by the injected server socket if (socketInjector != null) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java index 120d5dc9..83746a94 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java @@ -37,6 +37,7 @@ public class PlayerInjectorBuilder { protected ListenerInvoker invoker; protected Set packetListeners; protected Server server; + protected boolean alternativeJVM; /** * Set the class loader to use during class generation. @@ -107,6 +108,16 @@ public class PlayerInjectorBuilder { return this; } + /** + * Set whether or not the current JVM implementation is alternative. + * @param value - TRUE if it is, FALSE otherwise. + * @return The current builder, for chaining. + */ + public PlayerInjectorBuilder alternativeJVM(boolean value) { + alternativeJVM = value; + return this; + } + /** * Called before an object is created with this builder. */ @@ -140,6 +151,6 @@ public class PlayerInjectorBuilder { return new ProxyPlayerInjectionHandler( classLoader, reporter, injectionFilter, - invoker, packetListeners, server); + invoker, packetListeners, server, alternativeJVM); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java index a713a9d0..62da9e9b 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java @@ -36,7 +36,8 @@ import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PlayerLoggedOutException; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; -import com.comphenix.protocol.injector.server.InputStreamPlayerLookup; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; +import com.comphenix.protocol.injector.server.InputStreamLookupBuilder; import com.comphenix.protocol.injector.server.SocketInjector; import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; import com.google.common.base.Predicate; @@ -52,7 +53,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { private InjectedServerConnection serverInjection; // Server socket injection - private InputStreamPlayerLookup serverSocket; + private AbstractInputStreamLookup inputStreamLookup; // NetLogin injector private NetLoginInjector netLoginInjector; @@ -88,25 +89,33 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { // Used to filter injection attempts private Predicate injectionFilter; - public ProxyPlayerInjectionHandler(ClassLoader classLoader, ErrorReporter reporter, Predicate injectionFilter, - ListenerInvoker invoker, Set packetListeners, Server server) { + public ProxyPlayerInjectionHandler( + ClassLoader classLoader, ErrorReporter reporter, Predicate injectionFilter, + ListenerInvoker invoker, Set packetListeners, Server server, boolean alternateJVM) { this.classLoader = classLoader; this.reporter = reporter; this.invoker = invoker; this.injectionFilter = injectionFilter; this.packetListeners = packetListeners; - this.serverSocket = new InputStreamPlayerLookup(reporter, server); - this.netLoginInjector = new NetLoginInjector(reporter, this, serverSocket); - this.serverInjection = new InjectedServerConnection(reporter, serverSocket, server, netLoginInjector); + + this.inputStreamLookup = InputStreamLookupBuilder.newBuilder(). + server(server). + reporter(reporter). + alternativeJVM(alternateJVM). + build(); + + // Create net login injectors and the server connection injector + this.netLoginInjector = new NetLoginInjector(reporter, this, inputStreamLookup); + this.serverInjection = new InjectedServerConnection(reporter, inputStreamLookup, server, netLoginInjector); serverInjection.injectList(); } @Override public void postWorldLoaded() { // This will actually create a socket and a seperate thread ... - if (serverSocket != null) { - serverSocket.postWorldLoaded(); + if (inputStreamLookup != null) { + inputStreamLookup.postWorldLoaded(); } } @@ -208,7 +217,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { @Override public Player getPlayerByConnection(DataInputStream inputStream) { // Wait until the connection owner has been established - SocketInjector injector = serverSocket.getSocketInjector(inputStream); + SocketInjector injector = inputStreamLookup.getSocketInjector(inputStream); if (injector != null) { return injector.getPlayer(); @@ -298,7 +307,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { Socket socket = injector.getSocket(); // Guard against NPE here too - SocketInjector previous = socket != null ? serverSocket.getSocketInjector(socket) : null; + SocketInjector previous = socket != null ? inputStreamLookup.getSocketInjector(socket) : null; // Close any previously associated hooks before we proceed if (previous != null) { @@ -308,7 +317,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { injector.injectManager(); // Save injector - serverSocket.setSocketInjector(inputStream, injector); + inputStreamLookup.setSocketInjector(inputStream, injector); break; } @@ -435,7 +444,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { @Override public boolean uninjectPlayer(InetSocketAddress address) { if (!hasClosed && address != null) { - SocketInjector injector = serverSocket.getSocketInjector(address); + SocketInjector injector = inputStreamLookup.getSocketInjector(address); // Clean up if (injector != null) @@ -504,7 +513,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { // Only accept it if it's a player injector if (!(socket instanceof PlayerInjector)) { - socket = serverSocket.getSocketInjector(player.getAddress()); + socket = inputStreamLookup.getSocketInjector(player.getAddress()); } // Ensure that it is a player injector @@ -597,13 +606,13 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { } // Remove server handler - if (serverSocket != null) - serverSocket.cleanupAll(); + if (inputStreamLookup != null) + inputStreamLookup.cleanupAll(); if (serverInjection != null) serverInjection.cleanupAll(); if (netLoginInjector != null) netLoginInjector.cleanupAll(); - serverSocket = null; + inputStreamLookup = null; serverInjection = null; netLoginInjector = null; hasClosed = true; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java new file mode 100644 index 00000000..ed2a636a --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java @@ -0,0 +1,148 @@ +package com.comphenix.protocol.injector.server; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.concurrent.ConcurrentMap; + +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.google.common.collect.MapMaker; + +public abstract class AbstractInputStreamLookup { + // Used to access the inner input stream of a filtered input stream + private static Field filteredInputField; + + // Using weak keys and values ensures that we will not hold up garbage collection + protected ConcurrentMap ownerSocket = new MapMaker().weakKeys().makeMap(); + protected ConcurrentMap addressLookup = new MapMaker().weakValues().makeMap(); + + // Error reporter + protected final ErrorReporter reporter; + + // Reference to the server itself + protected final Server server; + + protected AbstractInputStreamLookup(ErrorReporter reporter, Server server) { + this.reporter = reporter; + this.server = server; + } + + /** + * Retrieve the underlying input stream that is associated with a given filter input stream. + * @param filtered - the filter input stream. + * @return The underlying input stream that is being filtered. + * @throws FieldAccessException Unable to access input stream. + */ + protected static InputStream getInputStream(FilterInputStream filtered) { + if (filteredInputField == null) + filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true). + getFieldByType("in", InputStream.class); + + InputStream current = filtered; + + try { + // Iterate until we find the real input stream + while (current instanceof FilterInputStream) { + current = (InputStream) FieldUtils.readField(filteredInputField, current, true); + } + return current; + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot access filtered input field.", e); + } + } + + /** + * Inject the given server thread or dedicated connection. + * @param container - class that contains a ServerSocket field. + */ + public abstract void inject(Object container); + + /** + * Invoked when the world has loaded. + */ + public abstract void postWorldLoaded(); + + /** + * Retrieve the associated socket injector for a player. + * @param filtered - the indentifying filtered input stream. + * @return The socket injector we have associated with this player. + * @throws FieldAccessException Unable to access input stream. + */ + public SocketInjector getSocketInjector(FilterInputStream filtered) { + return getSocketInjector(getInputStream(filtered)); + } + + /** + * Retrieve the associated socket injector for a player. + * @param filtered - the indentifying filtered input stream. + * @return The socket injector we have associated with this player. + */ + public abstract SocketInjector getSocketInjector(InputStream input); + + /** + * Retrieve a injector by its address. + * @param address - the address of the socket. + * @return The socket injector. + */ + public abstract SocketInjector getSocketInjector(SocketAddress address); + + /** + * Retrieve an injector by its socket. + * @param socket - the socket. + * @return The socket injector. + */ + public SocketInjector getSocketInjector(Socket socket) { + if (socket == null) + throw new IllegalArgumentException("The socket cannot be NULL."); + return getSocketInjector(socket.getRemoteSocketAddress()); + } + + /** + * Associate a given input stream with the provided socket injector. + * @param input - the filtered input stream to associate. + * @param injector - the injector. + * @throws FieldAccessException Unable to access input stream. + */ + public void setSocketInjector(FilterInputStream input, SocketInjector injector) { + setSocketInjector(getInputStream(input), injector); + } + + /** + * Associate a given input stream with the provided socket injector. + * @param input - the input stream to associate. + * @param injector - the injector. + */ + public void setSocketInjector(InputStream input, SocketInjector injector) { + SocketInjector previous = ownerSocket.put(input, injector); + + // Any previous temporary players will also be associated + if (previous != null) { + Player player = previous.getPlayer(); + + if (player instanceof InjectContainer) { + InjectContainer container = (InjectContainer) player; + container.setInjector(injector); + } + + // Update the reference to any previous injector + onPreviousSocketOverwritten(previous, injector); + } + } + + protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) { + // Do nothing + } + + /** + * Invoked when the injection should be undone. + */ + public abstract void cleanupAll(); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InjectContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InjectContainer.java index 24149a35..11cb476b 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InjectContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InjectContainer.java @@ -14,6 +14,8 @@ class InjectContainer { } public void setInjector(SocketInjector injector) { + if (injector == null) + throw new IllegalArgumentException("Injector cannot be NULL."); this.injector = injector; } } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamLookupBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamLookupBuilder.java new file mode 100644 index 00000000..6c156059 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamLookupBuilder.java @@ -0,0 +1,61 @@ +package com.comphenix.protocol.injector.server; + +import org.bukkit.Server; + +import com.comphenix.protocol.error.ErrorReporter; + +/** + * Constructs the appropriate input stream lookup for the current JVM and architecture. + * + * @author Kristian + */ +public class InputStreamLookupBuilder { + public static InputStreamLookupBuilder newBuilder() { + return new InputStreamLookupBuilder(); + } + + protected InputStreamLookupBuilder() { + // Use the static method. + } + + private Server server; + private ErrorReporter reporter; + private boolean alternativeJVM; + + /** + * Set the server instance to use. + * @param server - server instance. + * @return The current builder, for chaining. + */ + public InputStreamLookupBuilder server(Server server) { + this.server = server; + return this; + } + + /** + * Set the error reporter to pass on to the lookup. + * @param reporter - the error reporter. + * @return The current builder, for chaining. + */ + public InputStreamLookupBuilder reporter(ErrorReporter reporter) { + this.reporter = reporter; + return this; + } + + /** + * Set whether or not the current JVM implementation is alternative. + * @param value - TRUE if it is, FALSE otherwise. + * @return The current builder, for chaining. + */ + public InputStreamLookupBuilder alternativeJVM(boolean value) { + alternativeJVM = value; + return this; + } + + public AbstractInputStreamLookup build() { + if (alternativeJVM) + return new InputStreamProxyLookup(reporter, server); + else + return new InputStreamReflectLookup(reporter, server); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamPlayerLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamPlayerLookup.java deleted file mode 100644 index 5fb38abd..00000000 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamPlayerLookup.java +++ /dev/null @@ -1,435 +0,0 @@ -package com.comphenix.protocol.injector.server; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketAddress; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; - -import org.bukkit.Server; -import org.bukkit.entity.Player; - -import com.comphenix.protocol.Packets; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.reflect.FieldAccessException; -import com.comphenix.protocol.reflect.FieldUtils; -import com.comphenix.protocol.reflect.FuzzyReflection; -import com.comphenix.protocol.reflect.VolatileField; -import com.google.common.collect.MapMaker; - -/** - * Injection hook used to determine which Socket, and thus address, created any given DataInputStream. - * - * @author Kristian - */ -public class InputStreamPlayerLookup { - /** - * The read and connect timeout for our built-in MOTD reader. - */ - private static final int READ_TIMEOUT = 5000; - private static final int CONNECT_TIMEOUT = 1000; - - /** - * Represents a single send packet command. - * @author Kristian - */ - private static class SendPacketCommand { - private final Object packet; - private final boolean filtered; - - public SendPacketCommand(Object packet, boolean filtered) { - this.packet = packet; - this.filtered = filtered; - } - - public Object getPacket() { - return packet; - } - - public boolean isFiltered() { - return filtered; - } - } - - private static class TemporarySocketInjector implements SocketInjector { - private Player temporaryPlayer; - private Socket socket; - - // Queue of server packets - private List syncronizedQueue = Collections.synchronizedList(new ArrayList()); - - /** - * Represents a temporary socket injector. - * @param temporaryPlayer - temporary player instance. - * @param socket - the socket we are representing. - * @param fake - whether or not this connection should be ignored. - */ - public TemporarySocketInjector(Player temporaryPlayer, Socket socket) { - this.temporaryPlayer = temporaryPlayer; - this.socket = socket; - } - - @Override - public Socket getSocket() throws IllegalAccessException { - return socket; - } - - @Override - public SocketAddress getAddress() throws IllegalAccessException { - if (socket != null) - return socket.getRemoteSocketAddress(); - return null; - } - - @Override - public void disconnect(String message) throws InvocationTargetException { - // We have no choice - disregard message too - try { - socket.close(); - } catch (IOException e) { - throw new InvocationTargetException(e); - } - } - - @Override - public void sendServerPacket(Object packet, boolean filtered) - throws InvocationTargetException { - SendPacketCommand command = new SendPacketCommand(packet, filtered); - - // Queue until we can find something better - syncronizedQueue.add(command); - } - - @Override - public Player getPlayer() { - return temporaryPlayer; - } - - @Override - public Player getUpdatedPlayer() { - return temporaryPlayer; - } - - @Override - public void transferState(SocketInjector delegate) { - // Transmit all queued packets to a different injector. - try { - synchronized(syncronizedQueue) { - for (SendPacketCommand command : syncronizedQueue) { - delegate.sendServerPacket(command.getPacket(), command.isFiltered()); - } - syncronizedQueue.clear(); - } - } catch (InvocationTargetException e) { - throw new RuntimeException("Unable to transmit packets to " + delegate + " from old injector.", e); - } - } - } - - // Used to access the inner input stream of a filtered input stream - private static Field filteredInputField; - - // Using weak keys and values ensures that we will not hold up garbage collection - private ConcurrentMap ownerSocket = new MapMaker().weakKeys().makeMap(); - private ConcurrentMap addressLookup = new MapMaker().weakValues().makeMap(); - - // Fake connections - private Set fakeConnections = Collections.newSetFromMap( - new MapMaker().weakKeys().makeMap() - ); - - // The server socket that has been injected - private VolatileField injectedServerSocket; - - // Reference to the server itself - private final Server server; - - // Error reporter - private final ErrorReporter reporter; - - // Used to create fake players - private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory(); - - public InputStreamPlayerLookup(ErrorReporter reporter, Server server) { - this.reporter = reporter; - this.server = server; - } - - /** - * Inject the given server thread or dedicated connection. - * @param container - class that contains a ServerSocket field. - */ - public void inject(Object container) { - if (injectedServerSocket != null) - throw new IllegalStateException("Can only inject once. Create a new object instead."); - - Field selected = FuzzyReflection.fromObject(container, true). - getFieldByType("serverSocket", ServerSocket.class); - injectedServerSocket = new VolatileField(selected, container, true); - - // Load socket - ServerSocket socket = (ServerSocket) injectedServerSocket.getValue(); - - // Make sure it exists - if (socket == null) { - throw new IllegalStateException("Cannot find socket to inject. Reference " + selected + " contains NULL."); - } - - // Next, let us create the injected server socket - try { - injectedServerSocket.setValue(new DelegatedServerSocket(socket) { - @Override - public Socket accept() throws IOException { - Socket accepted = super.accept(); - - if (fakeConnections.contains(accepted.getRemoteSocketAddress())) { - // Don't intercept this connection - return accepted; - } - - // Wrap the socket we return - return new DelegatedSocket(accepted) { - @Override - public InputStream getInputStream() throws IOException { - InputStream input = super.getInputStream(); - SocketAddress address = delegate.getRemoteSocketAddress(); - - // Make sure that the address is actually valid - if (address != null) { - InputStream previousStream = addressLookup. - putIfAbsent(delegate.getRemoteSocketAddress(), input); - - // Ensure that this is our first time - if (previousStream == null) { - // Create a new temporary player - Player temporaryPlayer = tempPlayerFactory.createTemporaryPlayer(server); - TemporarySocketInjector temporaryInjector = new TemporarySocketInjector(temporaryPlayer, delegate); - DelegatedSocketInjector socketInjector = new DelegatedSocketInjector(temporaryInjector); - - // Associate the socket with a given input stream - setSocketInjector(input, socketInjector); - } - } - return input; - } - }; - } - }); - - } catch (IOException e) { - throw new IllegalStateException("Unbound socket threw an exception. Should never occur.", e); - } - } - - /** - * Invoked when the world has loaded. - */ - public void postWorldLoaded() { - cycleServerPorts(); - } - - /** - * Invoked when we need to cycle the injected server port. - *

- * This uses a fairly significant hack - we connect to our own server. - */ - private void cycleServerPorts() { - final ServerSocket serverSocket = (ServerSocket) injectedServerSocket.getValue(); - final SocketAddress address = new InetSocketAddress("127.0.0.1", serverSocket.getLocalPort()); - - // Sorry - Thread consumeThread = new Thread("ProtocolLib - Hack Thread") { - @Override - public void run() { - Socket socket = null; - OutputStream output = null; - InputStream input = null; - InputStreamReader reader = null; - - try { - socket = new Socket(); - socket.connect(address, CONNECT_TIMEOUT); - - // Ignore packets from this connection - fakeConnections.add(socket.getLocalSocketAddress()); - - // Shouldn't take that long - socket.setSoTimeout(READ_TIMEOUT); - - // Retrieve sockets - output = socket.getOutputStream(); - input = socket.getInputStream(); - reader = new InputStreamReader(input, Charset.forName("UTF-16BE")); - - // Get the server to send a MOTD - output.write(new byte[] { (byte) 0xFE, (byte) 0x01 }); - - int packetId = input.read(); - int length = reader.read(); - - if (packetId != Packets.Server.KICK_DISCONNECT) { - throw new IOException("Invalid packet ID: " + packetId); - } - if (length <= 0) { - throw new IOException("Invalid string length."); - } - char[] chars = new char[length]; - - // Read all the characters - if (reader.read(chars, 0, length) != length) { - throw new IOException("Premature end of stream."); - } - - System.out.println("Read: " + new String(chars)); - - } catch (Exception e) { - reporter.reportWarning(this, "Cannot simulate MOTD.", e); - } finally { - try { - if (reader != null) - reader.close(); - if (input != null) - input.close(); - if (output != null) - output.close(); - if (socket != null) - socket.close(); - } catch (IOException e) { - reporter.reportWarning(this, "Cannot clean up socket.", e); - } - } - } - }; - consumeThread.start(); - } - - /** - * Retrieve the underlying input stream that is associated with a given filter input stream. - * @param filtered - the filter input stream. - * @return The underlying input stream that is being filtered. - * @throws FieldAccessException Unable to access input stream. - */ - private static InputStream getInputStream(FilterInputStream filtered) { - if (filteredInputField == null) - filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true). - getFieldByType("in", InputStream.class); - - InputStream current = filtered; - - try { - // Iterate until we find the real input stream - while (current instanceof FilterInputStream) { - current = (InputStream) FieldUtils.readField(filteredInputField, current, true); - } - return current; - } catch (IllegalAccessException e) { - throw new FieldAccessException("Cannot access filtered input field.", e); - } - } - - /** - * Retrieve the associated socket injector for a player. - * @param filtered - the indentifying filtered input stream. - * @return The socket injector we have associated with this player. - * @throws FieldAccessException Unable to access input stream. - */ - public SocketInjector getSocketInjector(FilterInputStream filtered) { - return getSocketInjector(getInputStream(filtered)); - } - - /** - * Retrieve the associated socket injector for a player. - * @param filtered - the indentifying filtered input stream. - * @return The socket injector we have associated with this player. - */ - public SocketInjector getSocketInjector(InputStream input) { - return ownerSocket.get(input); - } - - /** - * Retrieve a injector by its address. - * @param address - the address of the socket. - * @return The socket injector. - */ - public SocketInjector getSocketInjector(SocketAddress address) { - InputStream input = addressLookup.get(address); - - if (input != null) { - return ownerSocket.get(input); - } else { - return null; - } - } - - /** - * Retrieve an injector by its socket. - * @param socket - the socket. - * @return The socket injector. - */ - public SocketInjector getSocketInjector(Socket socket) { - if (socket == null) - throw new IllegalArgumentException("The socket cannot be NULL."); - return getSocketInjector(socket.getRemoteSocketAddress()); - } - - /** - * Associate a given input stream with the provided socket injector. - * @param input - the filtered input stream to associate. - * @param injector - the injector. - * @throws FieldAccessException Unable to access input stream. - */ - public void setSocketInjector(FilterInputStream input, SocketInjector injector) { - setSocketInjector(getInputStream(input), injector); - } - - /** - * Associate a given input stream with the provided socket injector. - * @param input - the input stream to associate. - * @param injector - the injector. - */ - public void setSocketInjector(InputStream input, SocketInjector injector) { - SocketInjector previous = ownerSocket.put(input, injector); - - // Any previous temporary players will also be associated - if (previous != null) { - Player player = previous.getPlayer(); - - if (player instanceof InjectContainer) { - InjectContainer container = (InjectContainer) player; - container.setInjector(injector); - } - - // Update the reference to any previous injector - if (previous instanceof DelegatedSocketInjector) { - DelegatedSocketInjector delegated = (DelegatedSocketInjector) previous; - - // Update the delegate - delegated.setDelegate(injector); - } - } - } - - /** - * Invoked when the injection should be undone. - */ - public void cleanupAll() { - if (injectedServerSocket != null && injectedServerSocket.isCurrentSet()) { - injectedServerSocket.revertValue(); - - // This is going to suck - cycleServerPorts(); - } - } -} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamProxyLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamProxyLookup.java new file mode 100644 index 00000000..060f0ea9 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamProxyLookup.java @@ -0,0 +1,233 @@ +package com.comphenix.protocol.injector.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Set; + +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.VolatileField; +import com.google.common.collect.MapMaker; + +/** + * Injection hook used to determine which Socket, and thus address, created any given DataInputStream. + * + * @author Kristian + */ +class InputStreamProxyLookup extends AbstractInputStreamLookup { + /** + * The read and connect timeout for our built-in MOTD reader. + */ + private static final int READ_TIMEOUT = 5000; + private static final int CONNECT_TIMEOUT = 1000; + + // Fake connections + private Set fakeConnections = Collections.newSetFromMap( + new MapMaker().weakKeys().makeMap() + ); + + // The server socket that has been injected + private VolatileField injectedServerSocket; + + // Used to create fake players + private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory(); + + public InputStreamProxyLookup(ErrorReporter reporter, Server server) { + super(reporter, server); + } + + @Override + public void inject(Object container) { + if (injectedServerSocket != null) + throw new IllegalStateException("Can only inject once. Create a new object instead."); + + Field selected = FuzzyReflection.fromObject(container, true). + getFieldByType("serverSocket", ServerSocket.class); + injectedServerSocket = new VolatileField(selected, container, true); + + // Load socket + ServerSocket socket = (ServerSocket) injectedServerSocket.getValue(); + + // Make sure it exists + if (socket == null) { + throw new IllegalStateException("Cannot find socket to inject. Reference " + selected + " contains NULL."); + } + + // Next, let us create the injected server socket + try { + injectedServerSocket.setValue(new DelegatedServerSocket(socket) { + @Override + public Socket accept() throws IOException { + Socket accepted = super.accept(); + + if (fakeConnections.contains(accepted.getRemoteSocketAddress())) { + // Don't intercept this connection + return accepted; + } + + // Wrap the socket we return + return new DelegatedSocket(accepted) { + @Override + public InputStream getInputStream() throws IOException { + InputStream input = super.getInputStream(); + SocketAddress address = delegate.getRemoteSocketAddress(); + + // Make sure that the address is actually valid + if (address != null) { + InputStream previousStream = addressLookup. + putIfAbsent(delegate.getRemoteSocketAddress(), input); + + // Ensure that this is our first time + if (previousStream == null) { + // Create a new temporary player + Player temporaryPlayer = tempPlayerFactory.createTemporaryPlayer(server); + TemporarySocketInjector temporaryInjector = new TemporarySocketInjector(temporaryPlayer, delegate); + DelegatedSocketInjector socketInjector = new DelegatedSocketInjector(temporaryInjector); + + // Update it + TemporaryPlayerFactory.setInjectorInPlayer(temporaryPlayer, socketInjector); + + // Associate the socket with a given input stream + setSocketInjector(input, socketInjector); + } + } + return input; + } + }; + } + }); + + } catch (IOException e) { + throw new IllegalStateException("Unbound socket threw an exception. Should never occur.", e); + } + } + + @Override + public SocketInjector getSocketInjector(InputStream input) { + return ownerSocket.get(input); + } + + @Override + public SocketInjector getSocketInjector(SocketAddress address) { + InputStream input = addressLookup.get(address); + + if (input != null) { + return ownerSocket.get(input); + } else { + return null; + } + } + + @Override + public void postWorldLoaded() { + cycleServerPorts(); + } + + /** + * Invoked when we need to cycle the injected server port. + *

+ * This uses a fairly significant hack - we connect to our own server. + */ + void cycleServerPorts() { + final ServerSocket serverSocket = (ServerSocket) injectedServerSocket.getValue(); + final SocketAddress address = new InetSocketAddress("127.0.0.1", serverSocket.getLocalPort()); + + // Sorry + Thread consumeThread = new Thread("ProtocolLib - Hack Thread") { + @Override + public void run() { + Socket socket = null; + OutputStream output = null; + InputStream input = null; + InputStreamReader reader = null; + + try { + socket = new Socket(); + socket.connect(address, CONNECT_TIMEOUT); + + // Ignore packets from this connection + fakeConnections.add(socket.getLocalSocketAddress()); + + // Shouldn't take that long + socket.setSoTimeout(READ_TIMEOUT); + + // Retrieve sockets + output = socket.getOutputStream(); + input = socket.getInputStream(); + reader = new InputStreamReader(input, Charset.forName("UTF-16BE")); + + // Get the server to send a MOTD + output.write(new byte[] { (byte) 0xFE, (byte) 0x01 }); + + int packetId = input.read(); + int length = reader.read(); + + if (packetId != Packets.Server.KICK_DISCONNECT) { + throw new IOException("Invalid packet ID: " + packetId); + } + if (length <= 0) { + throw new IOException("Invalid string length."); + } + char[] chars = new char[length]; + + // Read all the characters + if (reader.read(chars, 0, length) != length) { + throw new IOException("Premature end of stream."); + } + + System.out.println("Read: " + new String(chars)); + + } catch (Exception e) { + reporter.reportWarning(this, "Cannot simulate MOTD.", e); + } finally { + try { + if (reader != null) + reader.close(); + if (input != null) + input.close(); + if (output != null) + output.close(); + if (socket != null) + socket.close(); + } catch (IOException e) { + reporter.reportWarning(this, "Cannot clean up socket.", e); + } + } + } + }; + consumeThread.start(); + } + + @Override + protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) { + if (previous instanceof DelegatedSocketInjector) { + DelegatedSocketInjector delegated = (DelegatedSocketInjector) previous; + + // Update the delegate + delegated.setDelegate(current); + } + } + + @Override + public void cleanupAll() { + if (injectedServerSocket != null && injectedServerSocket.isCurrentSet()) { + injectedServerSocket.revertValue(); + + // This is going to suck + cycleServerPorts(); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java new file mode 100644 index 00000000..bb8230d1 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java @@ -0,0 +1,89 @@ +package com.comphenix.protocol.injector.server; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.Socket; +import java.net.SocketAddress; + +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; + +class InputStreamReflectLookup extends AbstractInputStreamLookup { + // Used to create fake players + private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory(); + + public InputStreamReflectLookup(ErrorReporter reporter, Server server) { + super(reporter, server); + } + + @Override + public void inject(Object container) { + // Do nothing + } + + @Override + public void postWorldLoaded() { + // Nothing again + } + + @Override + public SocketInjector getSocketInjector(InputStream input) { + SocketInjector injector = ownerSocket.get(input); + + if (injector != null) { + return injector; + } else { + try { + Socket socket = getSocket(input); + Player player = tempPlayerFactory.createTemporaryPlayer(server); + SocketInjector created = new TemporarySocketInjector(player, socket); + + // Update injector + TemporaryPlayerFactory.setInjectorInPlayer(player, created); + + // Save address too + addressLookup.put(socket.getRemoteSocketAddress(), input); + + // Associate the socket with a given input stream + setSocketInjector(input, created); + return created; + + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot find or access socket field for " + input, e); + } + } + } + + @Override + public SocketInjector getSocketInjector(SocketAddress address) { + InputStream input = addressLookup.get(address); + + if (input != null) + return getSocketInjector(input); + else + return null; + } + + @Override + public void cleanupAll() { + // Do nothing + } + + private static Socket getSocket(InputStream stream) throws IllegalAccessException { + if (stream instanceof FilterInputStream) { + return getSocket(getInputStream((FilterInputStream) stream)); + } else { + // Just do it + Field socketField = FuzzyReflection.fromObject(stream, true). + getFieldByType("socket", Socket.class); + + return (Socket) FieldUtils.readField(socketField, stream, true); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java index 75e5b49e..0e6b4f15 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java @@ -59,6 +59,15 @@ public class TemporaryPlayerFactory { return null; } + /** + * Set the player injector, if possible. + * @param player - the player to update. + * @param injector - the injector to store. + */ + public static void setInjectorInPlayer(Player player, SocketInjector injector) { + ((InjectContainer) player).setInjector(injector); + } + /** * Construct a temporary player that supports a subset of every player command. *

diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporarySocketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporarySocketInjector.java new file mode 100644 index 00000000..e9f40a85 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporarySocketInjector.java @@ -0,0 +1,108 @@ +package com.comphenix.protocol.injector.server; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.bukkit.entity.Player; + +class TemporarySocketInjector implements SocketInjector { + /** + * Represents a single send packet command. + * @author Kristian + */ + static class SendPacketCommand { + private final Object packet; + private final boolean filtered; + + public SendPacketCommand(Object packet, boolean filtered) { + this.packet = packet; + this.filtered = filtered; + } + + public Object getPacket() { + return packet; + } + + public boolean isFiltered() { + return filtered; + } + } + + private Player temporaryPlayer; + private Socket socket; + + // Queue of server packets + private List syncronizedQueue = Collections.synchronizedList(new ArrayList()); + + /** + * Represents a temporary socket injector. + * @param temporaryPlayer - temporary player instance. + * @param socket - the socket we are representing. + * @param fake - whether or not this connection should be ignored. + */ + public TemporarySocketInjector(Player temporaryPlayer, Socket socket) { + this.temporaryPlayer = temporaryPlayer; + this.socket = socket; + } + + @Override + public Socket getSocket() throws IllegalAccessException { + return socket; + } + + @Override + public SocketAddress getAddress() throws IllegalAccessException { + if (socket != null) + return socket.getRemoteSocketAddress(); + return null; + } + + @Override + public void disconnect(String message) throws InvocationTargetException { + // We have no choice - disregard message too + try { + socket.close(); + } catch (IOException e) { + throw new InvocationTargetException(e); + } + } + + @Override + public void sendServerPacket(Object packet, boolean filtered) + throws InvocationTargetException { + SendPacketCommand command = new SendPacketCommand(packet, filtered); + + // Queue until we can find something better + syncronizedQueue.add(command); + } + + @Override + public Player getPlayer() { + return temporaryPlayer; + } + + @Override + public Player getUpdatedPlayer() { + return temporaryPlayer; + } + + @Override + public void transferState(SocketInjector delegate) { + // Transmit all queued packets to a different injector. + try { + synchronized(syncronizedQueue) { + for (SendPacketCommand command : syncronizedQueue) { + delegate.sendServerPacket(command.getPacket(), command.isFiltered()); + } + syncronizedQueue.clear(); + } + } catch (InvocationTargetException e) { + throw new RuntimeException("Unable to transmit packets to " + delegate + " from old injector.", e); + } + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml index e3c48aff..2f5a61e6 100644 --- a/ProtocolLib/src/main/resources/config.yml +++ b/ProtocolLib/src/main/resources/config.yml @@ -17,6 +17,8 @@ global: # Disable version checking for the given Minecraft version. Backup your world first! ignore version check: + # Set to TRUE if you're on a custom JVM implementation and you're having problems + alternate jvm: false + # Override the starting injecting method - injection method: - \ No newline at end of file + injection method: \ No newline at end of file