This commit is contained in:
59
src/main/java/cc.fascinated/common/DNSUtils.java
Normal file
59
src/main/java/cc.fascinated/common/DNSUtils.java
Normal file
@ -0,0 +1,59 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.*;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@UtilityClass
|
||||
public final class DNSUtils {
|
||||
private static final String SRV_QUERY_PREFIX = "_minecraft._tcp.%s";
|
||||
|
||||
/**
|
||||
* Resolve the hostname to an {@link InetSocketAddress}.
|
||||
*
|
||||
* @param hostname the hostname to resolve
|
||||
* @return the resolved {@link InetSocketAddress}
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static InetSocketAddress resolveSRV(@NonNull String hostname) {
|
||||
Record[] records = new Lookup(SRV_QUERY_PREFIX.formatted(hostname), Type.SRV).run(); // Resolve SRV records
|
||||
if (records == null) { // No records exist
|
||||
return null;
|
||||
}
|
||||
String host = null;
|
||||
int port = -1;
|
||||
for (Record record : records) {
|
||||
SRVRecord srv = (SRVRecord) record;
|
||||
host = srv.getTarget().toString().replaceFirst("\\.$", "");
|
||||
port = srv.getPort();
|
||||
}
|
||||
return host == null ? null : new InetSocketAddress(host, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the hostname to an {@link InetAddress}.
|
||||
*
|
||||
* @param hostname the hostname to resolve
|
||||
* @return the resolved {@link InetAddress}
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static InetAddress resolveA(@NonNull String hostname) {
|
||||
Record[] records = new Lookup(hostname, Type.A).run(); // Resolve A records
|
||||
if (records == null) { // No records exist
|
||||
return null;
|
||||
}
|
||||
InetAddress address = null;
|
||||
for (Record record : records) {
|
||||
address = ((ARecord) record).getAddress();
|
||||
}
|
||||
return address;
|
||||
}
|
||||
}
|
42
src/main/java/cc.fascinated/common/IPUtils.java
Normal file
42
src/main/java/cc.fascinated/common/IPUtils.java
Normal file
@ -0,0 +1,42 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class IPUtils {
|
||||
/**
|
||||
* The headers that contain the IP.
|
||||
*/
|
||||
private static final String[] IP_HEADERS = new String[] {
|
||||
"CF-Connecting-IP",
|
||||
"X-Forwarded-For"
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the real IP from the given request.
|
||||
*
|
||||
* @param request the request
|
||||
* @return the real IP
|
||||
*/
|
||||
public static String getRealIp(HttpServletRequest request) {
|
||||
String ip = request.getRemoteAddr();
|
||||
for (String headerName : IP_HEADERS) {
|
||||
String header = request.getHeader(headerName);
|
||||
if (header == null) {
|
||||
continue;
|
||||
}
|
||||
if (!header.contains(",")) { // Handle single IP
|
||||
ip = header;
|
||||
break;
|
||||
}
|
||||
// Handle multiple IPs
|
||||
String[] ips = header.split(",");
|
||||
for (String ipHeader : ips) {
|
||||
ip = ipHeader;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
90
src/main/java/cc.fascinated/common/PlayerUtils.java
Normal file
90
src/main/java/cc.fascinated/common/PlayerUtils.java
Normal file
@ -0,0 +1,90 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import cc.fascinated.Main;
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import cc.fascinated.model.player.Skin;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.UUID;
|
||||
|
||||
@UtilityClass @Log4j2
|
||||
public class PlayerUtils {
|
||||
|
||||
/**
|
||||
* Gets the UUID from the string.
|
||||
*
|
||||
* @param id the id string
|
||||
* @return the UUID
|
||||
*/
|
||||
public static UUID getUuidFromString(String id) {
|
||||
UUID uuid;
|
||||
boolean isFullUuid = id.length() == 36;
|
||||
if (id.length() == 32 || isFullUuid) {
|
||||
try {
|
||||
uuid = isFullUuid ? UUID.fromString(id) : UUIDUtils.addDashes(id);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new BadRequestException("Invalid UUID provided: %s".formatted(id));
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the skin data from the URL.
|
||||
*
|
||||
* @return the skin data
|
||||
*/
|
||||
@SneakyThrows
|
||||
@JsonIgnore
|
||||
public static byte[] getSkinImage(String url) {
|
||||
HttpResponse<byte[]> response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder(URI.create(url)).build(),
|
||||
HttpResponse.BodyHandlers.ofByteArray());
|
||||
return response.body();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the part data from the skin.
|
||||
*
|
||||
* @return the part data
|
||||
*/
|
||||
public static byte[] getSkinPartBytes(Skin skin, Skin.Parts part, int size) {
|
||||
if (size == -1) {
|
||||
size = part.getDefaultSize();
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage()));
|
||||
if (image == null) {
|
||||
image = ImageIO.read(new ByteArrayInputStream(Skin.DEFAULT_SKIN.getSkinImage())); // Fallback to the default skin
|
||||
}
|
||||
// Get the part of the image (e.g. the head)
|
||||
BufferedImage partImage = image.getSubimage(part.getX(), part.getY(), part.getWidth(), part.getHeight());
|
||||
|
||||
// Scale the image
|
||||
BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType());
|
||||
Graphics2D graphics2D = scaledImage.createGraphics();
|
||||
graphics2D.drawImage(partImage, 0, 0, size, size, null);
|
||||
graphics2D.dispose();
|
||||
partImage = scaledImage;
|
||||
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
ImageIO.write(partImage, "png", byteArrayOutputStream);
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to get {} part bytes for {}", part.name(), skin.getUrl(), ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
18
src/main/java/cc.fascinated/common/Tuple.java
Normal file
18
src/main/java/cc.fascinated/common/Tuple.java
Normal file
@ -0,0 +1,18 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter @AllArgsConstructor
|
||||
public class Tuple<L, R> {
|
||||
|
||||
/**
|
||||
* The left value of the tuple.
|
||||
*/
|
||||
private final L left;
|
||||
|
||||
/**
|
||||
* The right value of the tuple.
|
||||
*/
|
||||
private final R right;
|
||||
}
|
25
src/main/java/cc.fascinated/common/UUIDUtils.java
Normal file
25
src/main/java/cc.fascinated/common/UUIDUtils.java
Normal file
@ -0,0 +1,25 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@UtilityClass
|
||||
public class UUIDUtils {
|
||||
|
||||
/**
|
||||
* Add dashes to a UUID.
|
||||
*
|
||||
* @param trimmed the UUID without dashes
|
||||
* @return the UUID with dashes
|
||||
*/
|
||||
@NonNull
|
||||
public static UUID addDashes(@NonNull String trimmed) {
|
||||
StringBuilder builder = new StringBuilder(trimmed);
|
||||
for (int i = 0, pos = 20; i < 4; i++, pos -= 4) {
|
||||
builder.insert(pos, "-");
|
||||
}
|
||||
return UUID.fromString(builder.toString());
|
||||
}
|
||||
}
|
41
src/main/java/cc.fascinated/common/WebRequest.java
Normal file
41
src/main/java/cc.fascinated/common/WebRequest.java
Normal file
@ -0,0 +1,41 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@UtilityClass
|
||||
public class WebRequest {
|
||||
|
||||
/**
|
||||
* The web client.
|
||||
*/
|
||||
private static final RestClient CLIENT = RestClient.builder()
|
||||
.requestFactory(new HttpComponentsClientHttpRequestFactory())
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Gets a response from the given URL.
|
||||
*
|
||||
* @param url the url
|
||||
* @return the response
|
||||
* @param <T> the type of the response
|
||||
*/
|
||||
public static <T> T getAsEntity(String url, Class<T> clazz) {
|
||||
try {
|
||||
ResponseEntity<T> profile = CLIENT.get()
|
||||
.uri(url)
|
||||
.retrieve()
|
||||
.toEntity(clazz);
|
||||
|
||||
if (profile.getStatusCode().isError()) {
|
||||
return null;
|
||||
}
|
||||
return profile.getBody();
|
||||
} catch (HttpClientErrorException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user