ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java

224 lines
8.3 KiB
Java

/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.injector.server;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import com.comphenix.protocol.utility.ByteBuddyFactory;
import net.bytebuddy.description.ByteCodeElement;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodCall;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.FieldValue;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.utility.ChatExtensions;
/**
* Create fake player instances that represents pre-authenticated clients.
*/
public class TemporaryPlayerFactory {
private static final Constructor<? extends Player> temporaryPlayerConstructor = setupProxyPlayerConstructor();
/**
* Retrieve the injector from a given player if it contains one.
* @param player - the player that may contain a reference to a player injector.
* @return The referenced player injector, or NULL if none can be found.
*/
public static SocketInjector getInjectorFromPlayer(Player player) {
if (player instanceof TemporaryPlayer) {
return ((TemporaryPlayer) player).getInjector();
}
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) {
((TemporaryPlayer) player).setInjector(injector);
}
/**
* Construct a temporary player that supports a subset of every player command.
* <p>
* Supported methods include:
* <ul>
* <li>getPlayer()</li>
* <li>getAddress()</li>
* <li>getServer()</li>
* <li>chat(String)</li>
* <li>sendMessage(String)</li>
* <li>sendMessage(String[])</li>
* <li>kickPlayer(String)</li>
* </ul>
* <p>
* Note that a temporary player has not yet been assigned a name, and thus cannot be
* uniquely identified. Use the address instead.
* @param server - the current server.
* @return A temporary player instance.
*/
public Player createTemporaryPlayer(final Server server) {
try {
return temporaryPlayerConstructor.newInstance(server);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access reflection.", e);
} catch (InstantiationException e) {
throw new RuntimeException("Cannot instantiate object.", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Error in invocation.", e);
}
}
private static Constructor<? extends Player> setupProxyPlayerConstructor()
{
final MethodDelegation implementation = MethodDelegation.to(new Object() {
@RuntimeType
public Object delegate(@This Object obj, @Origin Method method, @FieldValue("server") Server server,
@AllArguments Object... args) throws Throwable {
String methodName = method.getName();
SocketInjector injector = ((TemporaryPlayer) obj).getInjector();
if (injector == null)
throw new IllegalStateException("Unable to find injector.");
// Use the socket to get the address
else if (methodName.equals("getPlayer"))
return injector.getUpdatedPlayer();
else if (methodName.equals("getAddress"))
return injector.getAddress();
else if (methodName.equals("getServer"))
return server;
// Handle send message methods
if (methodName.equals("chat") || methodName.equals("sendMessage")) {
try {
Object argument = args[0];
// Dynamic overloading
if (argument instanceof String) {
return sendMessage(injector, (String) argument);
} else if (argument instanceof String[]) {
for (String message : (String[]) argument) {
sendMessage(injector, message);
}
return null;
}
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
// Also, handle kicking
if (methodName.equals("kickPlayer")) {
injector.disconnect((String) args[0]);
return null;
}
// The fallback instance
Player updated = injector.getUpdatedPlayer();
if (updated != obj && updated != null) {
return method.invoke(updated, args);
}
// Methods that are supported in the fallback instance
if (methodName.equals("isOnline"))
return injector.getSocket() != null && injector.getSocket().isConnected();
else if (methodName.equals("getName"))
return "UNKNOWN[" + injector.getSocket().getRemoteSocketAddress() + "]";
// Ignore all other methods
throw new UnsupportedOperationException(
"The method " + method.getName() + " is not supported for temporary players.");
}
});
final ElementMatcher.Junction<ByteCodeElement> callbackFilter = ElementMatchers.not(
ElementMatchers.isDeclaredBy(Object.class).or(ElementMatchers.isDeclaredBy(TemporaryPlayer.class)));
try {
final Constructor<?> constructor = ByteBuddyFactory.getInstance()
.createSubclass(TemporaryPlayer.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
.name(TemporaryPlayerFactory.class.getPackage().getName() + ".TemporaryPlayerInvocationHandler")
.implement(Player.class)
.defineField("server", Server.class, Visibility.PRIVATE)
.defineConstructor(Visibility.PUBLIC)
.withParameters(Server.class)
.intercept(MethodCall.invoke(TemporaryPlayer.class.getDeclaredConstructor())
.andThen(FieldAccessor.ofField("server").setsArgumentAt(0)))
.method(callbackFilter)
.intercept(implementation)
.make()
.load(ByteBuddyFactory.getInstance().getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
.getLoaded()
.getDeclaredConstructor(Server.class);
return (Constructor<? extends Player>) constructor;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Failed to find Temporary Player constructor!", e);
}
}
/**
* Construct a temporary player with the given associated socket injector.
* @param server - the parent server.
* @param injector - the referenced socket injector.
* @return The temporary player.
*/
public Player createTemporaryPlayer(Server server, SocketInjector injector) {
Player temporary = createTemporaryPlayer(server);
((TemporaryPlayer) temporary).setInjector(injector);
return temporary;
}
/**
* Send a message to the given client.
* @param injector - the injector representing the client.
* @param message - a message.
* @return Always NULL.
* @throws InvocationTargetException If the message couldn't be sent.
* @throws FieldAccessException If we were unable to construct the message packet.
*/
private static Object sendMessage(SocketInjector injector, String message) throws InvocationTargetException, FieldAccessException {
for (PacketContainer packet : ChatExtensions.createChatPackets(message)) {
injector.sendServerPacket(packet.getHandle(), null, false);
}
return null;
}
}