forked from MinecraftUtilities/Backend
Merge pull request 'Add very basic Java MC server impl' (#1) from Rainnny/minecraft-helper:master into master
Reviewed-on: Fascinated/minecraft-helper#1
This commit is contained in:
commit
ed3b7e3064
@ -0,0 +1,66 @@
|
|||||||
|
package cc.fascinated.common.packet;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a packet in the
|
||||||
|
* Minecraft Java protocol.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
* @see <a href="https://wiki.vg/Protocol">Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
public abstract class MinecraftJavaPacket {
|
||||||
|
/**
|
||||||
|
* Process this packet.
|
||||||
|
*
|
||||||
|
* @param inputStream the input stream to read from
|
||||||
|
* @param outputStream the output stream to write to
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public abstract void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a variable integer to the output stream.
|
||||||
|
*
|
||||||
|
* @param outputStream the output stream to write to
|
||||||
|
* @param paramInt the integer to write
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
protected final void writeVarInt(DataOutputStream outputStream, int paramInt) throws IOException {
|
||||||
|
while (true) {
|
||||||
|
if ((paramInt & 0xFFFFFF80) == 0) {
|
||||||
|
outputStream.writeByte(paramInt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
outputStream.writeByte(paramInt & 0x7F | 0x80);
|
||||||
|
paramInt >>>= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a variable integer from the input stream.
|
||||||
|
*
|
||||||
|
* @param inputStream the input stream to read from
|
||||||
|
* @return the integer that was read
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
protected final int readVarInt(@NonNull DataInputStream inputStream) throws IOException {
|
||||||
|
int i = 0;
|
||||||
|
int j = 0;
|
||||||
|
while (true) {
|
||||||
|
int k = inputStream.readByte();
|
||||||
|
i |= (k & 0x7F) << j++ * 7;
|
||||||
|
if (j > 5) {
|
||||||
|
throw new RuntimeException("VarInt too big");
|
||||||
|
}
|
||||||
|
if ((k & 0x80) != 128) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package cc.fascinated.common.packet.impl.java;
|
||||||
|
|
||||||
|
import cc.fascinated.common.packet.MinecraftJavaPacket;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This packet is sent by the client to the server to set
|
||||||
|
* the hostname, port, and protocol version of the client.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
* @see <a href="https://wiki.vg/Protocol#Handshake">Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @ToString
|
||||||
|
public final class JavaPacketHandshakingInSetProtocol extends MinecraftJavaPacket {
|
||||||
|
private static final byte ID = 0x00; // The ID of the packet
|
||||||
|
private static final int STATUS_HANDSHAKE = 1; // The status handshake ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hostname of the server.
|
||||||
|
*/
|
||||||
|
@NonNull private final String hostname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The port of the server.
|
||||||
|
*/
|
||||||
|
private final int port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The protocol version of the server.
|
||||||
|
*/
|
||||||
|
private final int protocolVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process this packet.
|
||||||
|
*
|
||||||
|
* @param inputStream the input stream to read from
|
||||||
|
* @param outputStream the output stream to write to
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException {
|
||||||
|
try (ByteArrayOutputStream handshakeBytes = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream handshake = new DataOutputStream(handshakeBytes)
|
||||||
|
) {
|
||||||
|
handshake.writeByte(ID); // Write the ID of the packet
|
||||||
|
writeVarInt(handshake, protocolVersion); // Write the protocol version
|
||||||
|
writeVarInt(handshake, hostname.length()); // Write the length of the hostname
|
||||||
|
handshake.writeBytes(hostname); // Write the hostname
|
||||||
|
handshake.writeShort(port); // Write the port
|
||||||
|
writeVarInt(handshake, STATUS_HANDSHAKE); // Write the status handshake ID
|
||||||
|
|
||||||
|
// Write the handshake bytes to the output stream
|
||||||
|
writeVarInt(outputStream, handshakeBytes.size());
|
||||||
|
outputStream.write(handshakeBytes.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package cc.fascinated.common.packet.impl.java;
|
||||||
|
|
||||||
|
import cc.fascinated.common.packet.MinecraftJavaPacket;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This packet is sent by the client to the server to request the
|
||||||
|
* status of the server. The server will respond with a json object
|
||||||
|
* containing the server's status.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
* @see <a href="https://wiki.vg/Protocol#Status_Request">Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public final class JavaPacketStatusInStart extends MinecraftJavaPacket {
|
||||||
|
private static final byte ID = 0x00; // The ID of the packet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response json from the server, null if none.
|
||||||
|
*/
|
||||||
|
private String response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process this packet.
|
||||||
|
*
|
||||||
|
* @param inputStream the input stream to read from
|
||||||
|
* @param outputStream the output stream to write to
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException {
|
||||||
|
// Send the status request
|
||||||
|
outputStream.writeByte(0x01); // Size of packet
|
||||||
|
outputStream.writeByte(ID);
|
||||||
|
|
||||||
|
// Read the status response
|
||||||
|
readVarInt(inputStream); // Size of the response
|
||||||
|
int id = readVarInt(inputStream);
|
||||||
|
if (id == -1) { // The stream was prematurely ended
|
||||||
|
throw new IOException("Server prematurely ended stream.");
|
||||||
|
} else if (id != ID) { // Invalid packet ID
|
||||||
|
throw new IOException("Server returned invalid packet ID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int length = readVarInt(inputStream); // Length of the response
|
||||||
|
if (length == -1) { // The stream was prematurely ended
|
||||||
|
throw new IOException("Server prematurely ended stream.");
|
||||||
|
} else if (length == 0) {
|
||||||
|
throw new IOException("Server returned unexpected value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the json response
|
||||||
|
byte[] data = new byte[length];
|
||||||
|
inputStream.readFully(data);
|
||||||
|
response = new String(data);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
package cc.fascinated.controller;
|
package cc.fascinated.controller;
|
||||||
|
|
||||||
|
import cc.fascinated.common.PlayerUtils;
|
||||||
import cc.fascinated.model.player.Player;
|
import cc.fascinated.model.player.Player;
|
||||||
import cc.fascinated.model.player.Skin;
|
import cc.fascinated.model.player.Skin;
|
||||||
import cc.fascinated.model.response.impl.InvalidPartResponse;
|
import cc.fascinated.model.response.impl.InvalidPartResponse;
|
||||||
import cc.fascinated.model.response.impl.PlayerNotFoundResponse;
|
import cc.fascinated.model.response.impl.PlayerNotFoundResponse;
|
||||||
import cc.fascinated.service.PlayerService;
|
import cc.fascinated.service.PlayerService;
|
||||||
import cc.fascinated.common.PlayerUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.CacheControl;
|
import org.springframework.http.CacheControl;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
18
src/main/java/cc/fascinated/controller/ServerController.java
Normal file
18
src/main/java/cc/fascinated/controller/ServerController.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package cc.fascinated.controller;
|
||||||
|
|
||||||
|
import cc.fascinated.model.server.MinecraftServer;
|
||||||
|
import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/server/")
|
||||||
|
public class ServerController {
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<MinecraftServer> getServer(@PathVariable String hostname) {
|
||||||
|
return ResponseEntity.ok(JavaMinecraftServerPinger.INSTANCE.ping(hostname, 25565));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package cc.fascinated.model.mojang;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
public final class JavaServerStatusToken {
|
||||||
|
private final String description;
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
package cc.fascinated.model.mojang;
|
package cc.fascinated.model.mojang;
|
||||||
|
|
||||||
import cc.fascinated.Main;
|
import cc.fascinated.Main;
|
||||||
import cc.fascinated.model.player.Cape;
|
|
||||||
import cc.fascinated.model.player.Skin;
|
|
||||||
import cc.fascinated.common.Tuple;
|
import cc.fascinated.common.Tuple;
|
||||||
import cc.fascinated.common.UUIDUtils;
|
import cc.fascinated.common.UUIDUtils;
|
||||||
|
import cc.fascinated.model.player.Cape;
|
||||||
|
import cc.fascinated.model.player.Skin;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package cc.fascinated.model.player;
|
package cc.fascinated.model.player;
|
||||||
|
|
||||||
import cc.fascinated.model.mojang.MojangProfile;
|
|
||||||
import cc.fascinated.common.Tuple;
|
import cc.fascinated.common.Tuple;
|
||||||
import cc.fascinated.common.UUIDUtils;
|
import cc.fascinated.common.UUIDUtils;
|
||||||
|
import cc.fascinated.model.mojang.MojangProfile;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package cc.fascinated.model.player;
|
package cc.fascinated.model.player;
|
||||||
|
|
||||||
import cc.fascinated.config.Config;
|
|
||||||
import cc.fascinated.common.PlayerUtils;
|
import cc.fascinated.common.PlayerUtils;
|
||||||
|
import cc.fascinated.config.Config;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
package cc.fascinated.model.server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
public final class JavaMinecraftServer extends MinecraftServer {
|
||||||
|
public JavaMinecraftServer(String hostname, int port, String motd) {
|
||||||
|
super(hostname, port, motd);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package cc.fascinated.model.server;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
public class MinecraftServer {
|
||||||
|
private final String hostname;
|
||||||
|
private final int port;
|
||||||
|
private final String motd;
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
package cc.fascinated.service;
|
package cc.fascinated.service;
|
||||||
|
|
||||||
|
import cc.fascinated.common.WebRequest;
|
||||||
import cc.fascinated.model.mojang.MojangProfile;
|
import cc.fascinated.model.mojang.MojangProfile;
|
||||||
import cc.fascinated.model.mojang.MojangUsernameToUuid;
|
import cc.fascinated.model.mojang.MojangUsernameToUuid;
|
||||||
import cc.fascinated.common.WebRequest;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package cc.fascinated.service;
|
package cc.fascinated.service;
|
||||||
|
|
||||||
|
import cc.fascinated.common.UUIDUtils;
|
||||||
import cc.fascinated.model.mojang.MojangProfile;
|
import cc.fascinated.model.mojang.MojangProfile;
|
||||||
import cc.fascinated.model.mojang.MojangUsernameToUuid;
|
import cc.fascinated.model.mojang.MojangUsernameToUuid;
|
||||||
import cc.fascinated.model.player.Player;
|
import cc.fascinated.model.player.Player;
|
||||||
import cc.fascinated.common.UUIDUtils;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import net.jodah.expiringmap.ExpirationPolicy;
|
import net.jodah.expiringmap.ExpirationPolicy;
|
||||||
import net.jodah.expiringmap.ExpiringMap;
|
import net.jodah.expiringmap.ExpiringMap;
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package cc.fascinated.service.pinger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
* @param <T> the type of server to ping
|
||||||
|
*/
|
||||||
|
public interface MinecraftServerPinger<T> {
|
||||||
|
T ping(String hostname, int port);
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package cc.fascinated.service.pinger.impl;
|
||||||
|
|
||||||
|
import cc.fascinated.Main;
|
||||||
|
import cc.fascinated.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
|
||||||
|
import cc.fascinated.common.packet.impl.java.JavaPacketStatusInStart;
|
||||||
|
import cc.fascinated.model.mojang.JavaServerStatusToken;
|
||||||
|
import cc.fascinated.model.server.JavaMinecraftServer;
|
||||||
|
import cc.fascinated.service.pinger.MinecraftServerPinger;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@Log4j2(topic = "Java Pinger")
|
||||||
|
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
|
||||||
|
public static final JavaMinecraftServerPinger INSTANCE = new JavaMinecraftServerPinger();
|
||||||
|
|
||||||
|
private static final int TIMEOUT = 3000; // The timeout for the socket
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JavaMinecraftServer ping(String hostname, int port) {
|
||||||
|
log.info("Pinging {}:{}...", hostname, port);
|
||||||
|
|
||||||
|
// Open a socket connection to the server
|
||||||
|
try (Socket socket = new Socket()) {
|
||||||
|
socket.setTcpNoDelay(true);
|
||||||
|
socket.connect(new InetSocketAddress(hostname, port), TIMEOUT);
|
||||||
|
|
||||||
|
// Open data streams to begin packet transaction
|
||||||
|
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
|
||||||
|
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
|
||||||
|
// Begin handshaking with the server
|
||||||
|
new JavaPacketHandshakingInSetProtocol(hostname, port, 47).process(inputStream, outputStream);
|
||||||
|
|
||||||
|
// Send the status request to the server, and await back the response
|
||||||
|
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
|
||||||
|
packetStatusInStart.process(inputStream, outputStream);
|
||||||
|
System.out.println("packetStatusInStart.getResponse() = " + packetStatusInStart.getResponse());
|
||||||
|
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
|
||||||
|
return new JavaMinecraftServer(hostname, port, token.getDescription());
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user