package net.glowstone.net;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.CharsetUtil;
import net.glowstone.GlowServer;
import net.glowstone.entity.GlowPlayer;
import net.glowstone.net.query.QueryHandler;
import net.glowstone.net.query.QueryServer;
import net.glowstone.util.Convert;
import org.bukkit.World;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.mock;
/**
* Tests for the minecraft query server.
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({ GlowServer.class, GlowPlayer.class, QueryServer.class })
public class QueryTest {
/**
* Test values taken from <a href="http://wiki.vg/Query">wiki.vg/Query</a>
*/
private static final byte[] HANDSHAKE_RECV = Convert.fromHex("FEFD0900000001");
private static final byte[] HANDSHAKE_SEND = Convert.fromHex("09000000013935313333303700");
private static final byte[] BASIC_STATS_RECV = Convert.fromHex("FEFD00000000010091295B");
private static final byte[] BASIC_STATS_SEND = Convert.fromHex("000000000141204D696E6563726166742053657276657200534D5000776F726C64003200323000DD633132372E302E302E3100");
private static final byte[] FULL_STATS_RECV = Convert.fromHex("FEFD00000000010091295B00000000");
private static final byte[] FULL_STATS_SEND = Convert.fromHex("000000000173706C69746E756D008000686F73746E616D65004120"
+ "4D696E656372616674205365727665720067616D657479706500534D500067616D655F6964004D494E4543524146540076657273696F6E00"
+ ByteBufUtil.hexDump(Unpooled.wrappedBuffer(GlowServer.GAME_VERSION.getBytes(CharsetUtil.UTF_8))) // Always use the recent game version
+ "00706C7567696E7300"
+ ByteBufUtil.hexDump(Unpooled.wrappedBuffer("Glowstone 123 on Bukkit xyz".getBytes(CharsetUtil.UTF_8))) // Added, the original example did not use the 'plugin' key
+ "006D617000776F726C64006E756D706C61796572730032006D6178706C617965727300323000686F7374706F727400323535363500686F73"
+ "746970003132372E302E302E31000001706C617965725F00006261726E657967616C6500566976616C6168656C7669670000");
private GlowServer glowServer;
private QueryServer server;
private ThreadLocalRandom random;
private InetSocketAddress address;
private boolean queryPlugins;
@Before
public void setup() throws Exception {
glowServer = mock(GlowServer.class);
CountDownLatch latch = new CountDownLatch(1);
this.queryPlugins = true;
server = new QueryServer(glowServer, latch, queryPlugins);
random = mock(ThreadLocalRandom.class);
PowerMockito.mockStatic(ThreadLocalRandom.class);
when(ThreadLocalRandom.current()).thenReturn(random);
address = InetSocketAddress.createUnresolved("somehost", 12345);
}
@After
public void tearDown() throws Exception {
server.shutdown();
}
@Test
public void testChallengeTokens() throws Exception {
assertFalse("Accepted random challenge token.", server.verifyChallengeToken(address, 54321));
when(random.nextInt()).thenReturn(12345);
int token1 = server.generateChallengeToken(address);
assertTrue("Did not add challenge token.", server.verifyChallengeToken(address, token1));
when(random.nextInt()).thenReturn(6789);
int token2 = server.generateChallengeToken(address);
assertFalse("Expired token accepted.", server.verifyChallengeToken(address, token1));
assertTrue("Did not add challenge token.", server.verifyChallengeToken(address, token2));
server.flushChallengeTokens();
assertFalse("Flush did not remove token.", server.verifyChallengeToken(address, token2));
}
@Test
public void testHandshake() throws Exception {
QueryHandler handler = new QueryHandler(server, queryPlugins);
when(random.nextInt()).thenReturn(9513307);
testChannelRead(handler, HANDSHAKE_RECV, HANDSHAKE_SEND);
}
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void testBasicStats() throws Exception {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
when(glowServer.getMotd()).thenReturn("A Minecraft Server");
when(glowServer.getOnlinePlayers()).thenReturn((List) Arrays.asList(new Object(), new Object()));
when(glowServer.getMaxPlayers()).thenReturn(20);
when(glowServer.getPort()).thenReturn(25565);
when(glowServer.getWorlds()).thenReturn(Arrays.asList(world));
when(glowServer.getIp()).thenReturn("");
QueryHandler handler = new QueryHandler(server, queryPlugins);
when(random.nextInt()).thenReturn(9513307);
server.generateChallengeToken(address);
testChannelRead(handler, BASIC_STATS_RECV, BASIC_STATS_SEND);
}
@Test
public void testFullStats() throws Exception {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
GlowPlayer p1 = mock(GlowPlayer.class);
GlowPlayer p2 = mock(GlowPlayer.class);
when(p1.getName()).thenReturn("barneygale");
when(p2.getName()).thenReturn("Vivalahelvig");
PluginManager pluginManager = mock(PluginManager.class);
when(glowServer.getMotd()).thenReturn("A Minecraft Server");
Mockito.doReturn(Arrays.asList(p1, p2)).when(glowServer).getOnlinePlayers();
when(glowServer.getMaxPlayers()).thenReturn(20);
when(glowServer.getPort()).thenReturn(25565);
when(glowServer.getWorlds()).thenReturn(Arrays.asList(world));
when(glowServer.getIp()).thenReturn("");
when(glowServer.getVersion()).thenReturn("123");
when(glowServer.getBukkitVersion()).thenReturn("xyz");
when(glowServer.getPluginManager()).thenReturn(pluginManager);
when(pluginManager.getPlugins()).thenReturn(new Plugin[0]);
QueryHandler handler = new QueryHandler(server, queryPlugins);
when(random.nextInt()).thenReturn(9513307);
server.generateChallengeToken(address);
testChannelRead(handler, FULL_STATS_RECV, FULL_STATS_SEND);
}
private void testChannelRead(QueryHandler handler, byte[] recv, byte[] send) throws Exception {
ChannelHandlerContext ctx = mock(ChannelHandlerContext.class);
ByteBufAllocator alloc = mock(ByteBufAllocator.class);
when(ctx.alloc()).thenReturn(alloc);
when(alloc.buffer()).thenReturn(Unpooled.buffer());
DatagramPacket packet = new DatagramPacket(Unpooled.wrappedBuffer(recv), null, address);
handler.channelRead(ctx, packet);
verify(ctx).write(argThat(new DatagramPacketMatcher(send)));
}
/**
* Matches the content (nothing else) of two {@link DatagramPacket}s.
*/
private class DatagramPacketMatcher extends BaseMatcher<DatagramPacket> {
private final byte[] content;
public DatagramPacketMatcher(byte[] content) {
this.content = content;
}
@Override
public boolean matches(Object obj) {
ByteBuf buf = ((DatagramPacket) obj).content();
byte[] array = new byte[buf.readableBytes()];
buf.readBytes(array);
buf.readerIndex(buf.readerIndex() - array.length);
return Arrays.equals(array, content);
}
@Override
public void describeTo(Description description) {
description.appendText("DatagramPacket(" + ByteBufUtil.hexDump(Unpooled.wrappedBuffer(content)) + ")");
}
}
}