Replace MCAuth with RK_01 MinecraftAuth (#795)

* Initial work on moving over mcauth

* Initial work on importing MinecraftAuth

* Make compile

* Remove extra headers code

* Switch to different http utils

* Merge changes

* Cleanup

* Remove unused exceptions and constructors

* Implement proxies

* Fixup proxy stuff

* Cleanup

* Remove SR license header

* Remove custom exceptions

* Move auth into main module

Auth has become so small that it's not worth keeping separate

* Make ProxyInfo be part of network again

* Fix indent

* Allow null id and name in GameProfile

* Fix remaining logs

* Make texture checker more accurate

* Fix spaces

* Update dependencies

* Remove usage of var

* Use faster approach for reading raw uuids.
This commit is contained in:
Alex 2024-06-17 22:23:42 +02:00 committed by GitHub
parent 5624d7729a
commit 471e92ec6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 908 additions and 135 deletions

View file

@ -0,0 +1,53 @@
package org.geysermc.mcprotocollib.auth.example;
import net.raphimc.minecraftauth.MinecraftAuth;
import net.raphimc.minecraftauth.step.java.StepMCProfile;
import net.raphimc.minecraftauth.step.java.StepMCToken;
import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession;
import net.raphimc.minecraftauth.step.msa.StepCredentialsMsaCode;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.auth.SessionService;
import org.geysermc.mcprotocollib.network.ProxyInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MinecraftAuthTest {
private static final Logger log = LoggerFactory.getLogger(MinecraftAuthTest.class);
private static final String EMAIL = "Username@mail.com";
private static final String PASSWORD = "Password";
private static final boolean REQUIRE_SECURE_TEXTURES = true;
private static final ProxyInfo PROXY = null;
public static void main(String[] args) {
auth();
}
private static void auth() {
SessionService service = new SessionService();
service.setProxy(PROXY);
StepFullJavaSession.FullJavaSession fullJavaSession;
try {
fullJavaSession = MinecraftAuth.JAVA_CREDENTIALS_LOGIN.getFromInput(
MinecraftAuth.createHttpClient(),
new StepCredentialsMsaCode.MsaCredentials(EMAIL, PASSWORD));
} catch (Exception e) {
throw new RuntimeException(e);
}
StepMCProfile.MCProfile mcProfile = fullJavaSession.getMcProfile();
StepMCToken.MCToken mcToken = mcProfile.getMcToken();
GameProfile profile = new GameProfile(mcProfile.getId(), mcProfile.getName());
try {
service.fillProfileProperties(profile);
log.info("Selected Profile: {}", profile);
log.info("Selected Profile Textures: {}", profile.getTextures(REQUIRE_SECURE_TEXTURES));
log.info("Access Token: {}", mcToken.getAccessToken());
log.info("Expire Time: {}", mcToken.getExpireTimeMs());
} catch (Exception e) {
log.error("Failed to get properties and textures of selected profile {}.", profile, e);
}
}
}

View file

@ -1,14 +1,16 @@
package org.geysermc.mcprotocollib.protocol.example;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.auth.exception.request.RequestException;
import com.github.steveice10.mc.auth.service.AuthenticationService;
import com.github.steveice10.mc.auth.service.MojangAuthenticationService;
import com.github.steveice10.mc.auth.service.SessionService;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.raphimc.minecraftauth.MinecraftAuth;
import net.raphimc.minecraftauth.step.java.StepMCProfile;
import net.raphimc.minecraftauth.step.java.StepMCToken;
import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession;
import net.raphimc.minecraftauth.step.msa.StepCredentialsMsaCode;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.auth.SessionService;
import org.geysermc.mcprotocollib.network.ProxyInfo;
import org.geysermc.mcprotocollib.network.Server;
import org.geysermc.mcprotocollib.network.Session;
@ -36,7 +38,6 @@ import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.Serverbound
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.Proxy;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
@ -51,7 +52,7 @@ public class MinecraftProtocolTest {
private static final String HOST = "127.0.0.1";
private static final int PORT = 25565;
private static final ProxyInfo PROXY = null;
private static final Proxy AUTH_PROXY = Proxy.NO_PROXY;
private static final ProxyInfo AUTH_PROXY = null;
private static final String USERNAME = "Username";
private static final String PASSWORD = "Password";
@ -177,19 +178,21 @@ public class MinecraftProtocolTest {
private static void login() {
MinecraftProtocol protocol;
if (VERIFY_USERS) {
StepFullJavaSession.FullJavaSession fullJavaSession;
try {
AuthenticationService authService = new MojangAuthenticationService();
authService.setUsername(USERNAME);
authService.setPassword(PASSWORD);
authService.setProxy(AUTH_PROXY);
authService.login();
protocol = new MinecraftProtocol(authService.getSelectedProfile(), authService.getAccessToken());
log.info("Successfully authenticated user.");
} catch (RequestException e) {
log.error("Failed to authenticate user.", e);
return;
fullJavaSession = MinecraftAuth.JAVA_CREDENTIALS_LOGIN.getFromInput(
MinecraftAuth.createHttpClient(),
new StepCredentialsMsaCode.MsaCredentials(USERNAME, PASSWORD));
} catch (Exception e) {
throw new RuntimeException(e);
}
StepMCProfile.MCProfile mcProfile = fullJavaSession.getMcProfile();
StepMCToken.MCToken mcToken = mcProfile.getMcToken();
protocol = new MinecraftProtocol(
new GameProfile(mcProfile.getId(), mcProfile.getName()),
mcToken.getAccessToken());
log.info("Successfully authenticated user.");
} else {
protocol = new MinecraftProtocol(USERNAME);
}

View file

@ -4,12 +4,13 @@ metadata.format.version = "1.1"
adventure = "4.15.0"
cloudburstnbt = "3.0.0.Final"
mcauthlib = "e5b0bcc"
slf4j = "2.0.9"
math = "2.0"
fastutil-maps = "8.5.3"
netty = "4.1.103.Final"
netty-io_uring = "0.0.24.Final"
gson = "2.11.0"
minecraftauth = "4.0.2"
checkerframework = "3.42.0"
junit = "5.8.2"
@ -24,7 +25,6 @@ adventure-text-serializer-gson = { module = "net.kyori:adventure-text-serializer
adventure-text-serializer-json-legacy-impl = { module = "net.kyori:adventure-text-serializer-json-legacy-impl", version.ref = "adventure" }
cloudburstnbt = { module = "org.cloudburstmc:nbt", version.ref = "cloudburstnbt" }
mcauthlib = { module = "com.github.GeyserMC:mcauthlib", version.ref = "mcauthlib" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
@ -39,6 +39,10 @@ fastutil-int2int-maps = { module = "com.nukkitx.fastutil:fastutil-int-int-maps",
netty-all = { module = "io.netty:netty-all", version.ref = "netty" }
netty-incubator-transport-native-io_uring = { module = "io.netty.incubator:netty-incubator-transport-native-io_uring", version.ref = "netty-io_uring" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
minecraftauth = { module = "net.raphimc:MinecraftAuth", version.ref = "minecraftauth" }
checkerframework-qual = { module = "org.checkerframework:checker-qual", version.ref = "checkerframework" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

View file

@ -9,7 +9,12 @@ description = "MCProtocolLib is a simple library for communicating with Minecraf
dependencies {
// Minecraft related libraries
api(libs.cloudburstnbt)
api(libs.mcauthlib)
// Gson
api(libs.gson)
// MinecraftAuth for authentication
api(libs.minecraftauth)
// Slf4j
api(libs.slf4j.api)

View file

@ -0,0 +1,453 @@
package org.geysermc.mcprotocollib.auth;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.geysermc.mcprotocollib.auth.util.TextureUrlChecker;
import org.geysermc.mcprotocollib.auth.util.UndashedUUIDAdapter;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
* Information about a user profile.
*/
public class GameProfile {
private static final PublicKey SIGNATURE_KEY = loadSignatureKey();
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(UUID.class, new UndashedUUIDAdapter())
.create();
private static PublicKey loadSignatureKey() {
try (InputStream in = Objects.requireNonNull(SessionService.class.getResourceAsStream("/yggdrasil_session_pubkey.der"))) {
return KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(in.readAllBytes()));
} catch (Exception e) {
throw new RuntimeException("Missing/invalid yggdrasil public key.", e);
}
}
private final UUID id;
private final String name;
private List<Property> properties;
private Map<TextureType, Texture> textures;
private boolean texturesVerified;
/**
* Creates a new GameProfile instance.
*
* @param id ID of the profile.
* @param name Name of the profile.
*/
public GameProfile(String id, String name) {
this(id == null || id.isEmpty() ? null : UUID.fromString(id), name);
}
/**
* Creates a new GameProfile instance.
*
* @param id ID of the profile.
* @param name Name of the profile.
*/
public GameProfile(UUID id, String name) {
this.id = id;
this.name = name;
}
/**
* Gets whether the profile is complete.
*
* @return Whether the profile is complete.
*/
public boolean isComplete() {
return this.id != null && this.name != null && !this.name.isEmpty();
}
/**
* Gets the ID of the profile.
*
* @return The profile's ID.
*/
public UUID getId() {
return this.id;
}
/**
* Gets the ID of the profile as a String.
*
* @return The profile's ID as a string.
*/
public String getIdAsString() {
return this.id != null ? this.id.toString() : "";
}
/**
* Gets the name of the profile.
*
* @return The profile's name.
*/
public String getName() {
return this.name;
}
/**
* Gets an immutable list of properties contained in the profile.
*
* @return The profile's properties.
*/
public List<Property> getProperties() {
if (this.properties == null) {
this.properties = new ArrayList<>();
}
return Collections.unmodifiableList(this.properties);
}
/**
* Sets the properties of this profile.
*
* @param properties Properties belonging to this profile.
*/
public void setProperties(List<Property> properties) {
if (this.properties == null) {
this.properties = new ArrayList<>();
} else {
this.properties.clear();
}
if (properties != null) {
this.properties.addAll(properties);
}
// Invalidate cached decoded textures.
this.textures = null;
this.texturesVerified = false;
}
/**
* Gets a property contained in the profile.
*
* @param name Name of the property.
* @return The property with the specified name.
*/
public Property getProperty(String name) {
for (Property property : this.getProperties()) {
if (property.getName().equals(name)) {
return property;
}
}
return null;
}
/**
* Gets an immutable map of texture types to textures contained in the profile.
*
* @return The profile's textures.
* @throws IllegalStateException If an error occurs decoding the profile's texture property.
*/
public Map<TextureType, Texture> getTextures() throws IllegalStateException {
return this.getTextures(true);
}
/**
* Gets an immutable map of texture types to textures contained in the profile.
*
* @param requireSecure Whether to require the profile's texture payload to be securely signed.
* @return The profile's textures.
* @throws IllegalStateException If an error occurs decoding the profile's texture property.
*/
public Map<TextureType, Texture> getTextures(boolean requireSecure) throws IllegalStateException {
if (this.textures == null || (requireSecure && !this.texturesVerified)) {
GameProfile.Property textures = this.getProperty("textures");
if (textures != null) {
if (requireSecure) {
if (!textures.hasSignature()) {
throw new IllegalStateException("Signature is missing from textures payload.");
}
if (!textures.isSignatureValid(SIGNATURE_KEY)) {
throw new IllegalStateException("Textures payload has been tampered with. (signature invalid)");
}
}
MinecraftTexturesPayload result;
try {
String json = new String(Base64.getDecoder().decode(textures.getValue().getBytes(StandardCharsets.UTF_8)));
result = GSON.fromJson(json, MinecraftTexturesPayload.class);
} catch (Exception e) {
throw new IllegalStateException("Could not decode texture payload.", e);
}
if (result != null && result.textures != null) {
if (requireSecure) {
for (GameProfile.Texture texture : result.textures.values()) {
if (TextureUrlChecker.isAllowedTextureDomain(texture.getURL())) {
continue;
}
throw new IllegalStateException("Textures payload has been tampered with. (non-whitelisted domain)");
}
}
this.textures = result.textures;
} else {
this.textures = Collections.emptyMap();
}
this.texturesVerified = requireSecure;
} else {
return Collections.emptyMap();
}
}
return Collections.unmodifiableMap(this.textures);
}
/**
* Gets a texture contained in the profile.
*
* @param type Type of texture to get.
* @return The texture of the specified type.
* @throws IllegalStateException If an error occurs decoding the profile's texture property.
*/
public Texture getTexture(TextureType type) throws IllegalStateException {
return this.getTextures().get(type);
}
/**
* Gets a texture contained in the profile.
*
* @param type Type of texture to get.
* @param requireSecure Whether to require the profile's texture payload to be securely signed.
* @return The texture of the specified type.
* @throws IllegalStateException If an error occurs decoding the profile's texture property.
*/
public Texture getTexture(TextureType type, boolean requireSecure) throws IllegalStateException {
return this.getTextures(requireSecure).get(type);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (o != null && this.getClass() == o.getClass()) {
GameProfile that = (GameProfile) o;
return Objects.equals(this.id, that.id) && Objects.equals(this.name, that.name);
} else {
return false;
}
}
@Override
public int hashCode() {
int result = this.id != null ? this.id.hashCode() : 0;
result = 31 * result + (this.name != null ? this.name.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "GameProfile{id=" + this.id + ", name=" + this.name + ", properties=" + this.getProperties() + "}";
}
/**
* A property belonging to a profile.
*/
public static class Property {
private final String name;
private final String value;
private final String signature;
/**
* Creates a new Property instance.
*
* @param name Name of the property.
* @param value Value of the property.
*/
public Property(String name, String value) {
this(name, value, null);
}
/**
* Creates a new Property instance.
*
* @param name Name of the property.
* @param value Value of the property.
* @param signature Signature used to verify the property.
*/
public Property(String name, String value, String signature) {
this.name = name;
this.value = value;
this.signature = signature;
}
/**
* Gets the name of the property.
*
* @return The property's name.
*/
public String getName() {
return this.name;
}
/**
* Gets the value of the property.
*
* @return The property's value.
*/
public String getValue() {
return this.value;
}
/**
* Gets whether this property has a signature to verify it.
*
* @return Whether this property is signed.
*/
public boolean hasSignature() {
return this.signature != null;
}
/**
* Gets the signature used to verify the property.
*
* @return The property's signature.
*/
public String getSignature() {
return this.signature;
}
/**
* Gets whether this property's signature is valid.
*
* @param key Public key to validate the signature against.
* @return Whether the signature is valid.
* @throws IllegalStateException If the signature could not be validated.
*/
public boolean isSignatureValid(PublicKey key) throws IllegalStateException {
if (!this.hasSignature()) {
return false;
}
try {
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(key);
sig.update(this.value.getBytes());
return sig.verify(Base64.getDecoder().decode(this.signature.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IllegalStateException("Could not validate property signature.", e);
}
}
@Override
public String toString() {
return "Property{name=" + this.name + ", value=" + this.value + ", signature=" + this.signature + "}";
}
}
/**
* The type of a profile texture.
*/
public enum TextureType {
SKIN,
CAPE,
ELYTRA;
}
/**
* The model used for a profile texture.
*/
public enum TextureModel {
NORMAL,
SLIM;
}
/**
* A texture contained within a profile.
*/
public static class Texture {
private final String url;
private final Map<String, String> metadata;
/**
* Creates a new Texture instance.
*
* @param url URL of the texture.
* @param metadata Metadata of the texture.
*/
public Texture(String url, Map<String, String> metadata) {
this.url = url;
this.metadata = new HashMap<>(metadata);
}
/**
* Gets the URL of the texture.
*
* @return The texture's URL.
*/
public String getURL() {
return this.url;
}
/**
* Gets a metadata string from the texture.
*
* @return The metadata value corresponding to the given key.
*/
public String getMetadata(String key) {
return this.metadata.get(key);
}
/**
* Gets the model of the texture.
*
* @return The texture's model.
*/
public TextureModel getModel() {
String model = this.getMetadata("model");
return model != null && model.equals("slim") ? TextureModel.SLIM : TextureModel.NORMAL;
}
/**
* Gets the hash of the texture.
*
* @return The texture's hash.
*/
public String getHash() {
String url = this.url.endsWith("/") ? this.url.substring(0, this.url.length() - 1) : this.url;
int slash = url.lastIndexOf("/");
int dot = url.lastIndexOf(".");
if (dot < slash) {
dot = url.length();
}
return url.substring(slash + 1, dot != -1 ? dot : url.length());
}
@Override
public String toString() {
return "Texture{url=" + this.url + ", model=" + this.getModel() + ", hash=" + this.getHash() + "}";
}
}
private static class MinecraftTexturesPayload {
public long timestamp;
public UUID profileId;
public String profileName;
public boolean isPublic;
public Map<GameProfile.TextureType, GameProfile.Texture> textures;
}
}

View file

@ -0,0 +1,126 @@
package org.geysermc.mcprotocollib.auth;
import lombok.Getter;
import lombok.Setter;
import org.geysermc.mcprotocollib.auth.util.HTTPUtils;
import org.geysermc.mcprotocollib.network.ProxyInfo;
import org.geysermc.mcprotocollib.auth.util.UUIDUtils;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.util.List;
import java.util.UUID;
/**
* Service used for session-related queries.
*/
@Setter
@Getter
public class SessionService {
private static final URI JOIN_ENDPOINT = URI.create("https://sessionserver.mojang.com/session/minecraft/join");
private static final String HAS_JOINED_ENDPOINT = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s";
private static final String PROFILE_ENDPOINT = "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false";
private ProxyInfo proxy;
/**
* Calculates the server ID from a base string, public key, and secret key.
*
* @param base Base server ID to use.
* @param publicKey Public key to use.
* @param secretKey Secret key to use.
* @return The calculated server ID.
* @throws IllegalStateException If the server ID hash algorithm is unavailable.
*/
public static String getServerId(String base, PublicKey publicKey, SecretKey secretKey) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(base.getBytes(StandardCharsets.ISO_8859_1));
digest.update(secretKey.getEncoded());
digest.update(publicKey.getEncoded());
return new BigInteger(digest.digest()).toString(16);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Server ID hash algorithm unavailable.", e);
}
}
/**
* Joins a server.
*
* @param profile Profile to join the server with.
* @param authenticationToken Authentication token to join the server with.
* @param serverId ID of the server to join.
* @throws IOException If an error occurs while making the request.
*/
public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws IOException {
JoinServerRequest request = new JoinServerRequest(authenticationToken, profile.getId(), serverId);
HTTPUtils.makeRequest(this.getProxy(), JOIN_ENDPOINT, request, null);
}
/**
* Gets the profile of the given user if they are currently logged in to the given server.
*
* @param name Name of the user to get the profile of.
* @param serverId ID of the server to check if they're logged in to.
* @return The profile of the given user, or null if they are not logged in to the given server.
* @throws IOException If an error occurs while making the request.
*/
public GameProfile getProfileByServer(String name, String serverId) throws IOException {
HasJoinedResponse response = HTTPUtils.makeRequest(this.getProxy(),
URI.create(String.format(HAS_JOINED_ENDPOINT,
URLEncoder.encode(name, StandardCharsets.UTF_8),
URLEncoder.encode(serverId, StandardCharsets.UTF_8))),
null, HasJoinedResponse.class);
if (response != null && response.id != null) {
GameProfile result = new GameProfile(response.id, name);
result.setProperties(response.properties);
return result;
} else {
return null;
}
}
/**
* Fills in the properties of a profile.
*
* @param profile Profile to fill in the properties of.
* @throws IOException If the property lookup fails.
*/
public void fillProfileProperties(GameProfile profile) throws IOException {
if (profile.getId() == null) {
return;
}
MinecraftProfileResponse response = HTTPUtils.makeRequest(this.getProxy(), URI.create(String.format(PROFILE_ENDPOINT, UUIDUtils.convertToNoDashes(profile.getId()))), null, MinecraftProfileResponse.class);
if (response == null) {
throw new IllegalStateException("Couldn't fetch profile properties for " + profile + " as the profile does not exist.");
}
profile.setProperties(response.properties);
}
@Override
public String toString() {
return "SessionService{}";
}
private record JoinServerRequest(String accessToken, UUID selectedProfile, String serverId) {
}
private static class HasJoinedResponse {
public UUID id;
public List<GameProfile.Property> properties;
}
private static class MinecraftProfileResponse {
public UUID id;
public String name;
public List<GameProfile.Property> properties;
}
}

View file

@ -0,0 +1,72 @@
package org.geysermc.mcprotocollib.auth.util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.lenni0451.commons.httpclient.HttpClient;
import net.lenni0451.commons.httpclient.HttpResponse;
import net.lenni0451.commons.httpclient.constants.ContentTypes;
import net.lenni0451.commons.httpclient.constants.Headers;
import net.lenni0451.commons.httpclient.content.HttpContent;
import net.lenni0451.commons.httpclient.proxy.ProxyHandler;
import net.lenni0451.commons.httpclient.proxy.ProxyType;
import net.lenni0451.commons.httpclient.requests.HttpContentRequest;
import net.lenni0451.commons.httpclient.requests.HttpRequest;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.mcprotocollib.network.ProxyInfo;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.UUID;
/**
* Utilities for making HTTP requests.
*/
public class HTTPUtils {
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(UUID.class, new UndashedUUIDAdapter())
.create();
private HTTPUtils() {
}
public static <T> T makeRequest(@Nullable ProxyInfo proxy, URI uri, Object input, Class<T> responseType) throws IOException {
if (proxy == null) {
throw new IllegalArgumentException("Proxy cannot be null.");
} else if (uri == null) {
throw new IllegalArgumentException("URI cannot be null.");
}
HttpResponse response = createHttpClient(proxy).execute(input == null ? new HttpRequest("GET", uri.toURL()) :
new HttpContentRequest("POST", uri.toURL()).setContent(HttpContent.string(GSON.toJson(input))));
if (responseType == null) {
return null;
}
return GSON.fromJson(new InputStreamReader(new ByteArrayInputStream(response.getContent())), responseType);
}
public static HttpClient createHttpClient(@Nullable ProxyInfo proxy) {
final int timeout = 5000;
HttpClient client = new HttpClient()
.setConnectTimeout(timeout)
.setReadTimeout(timeout * 2)
.setCookieManager(null)
.setFollowRedirects(false)
.setHeader(Headers.ACCEPT, ContentTypes.APPLICATION_JSON.toString())
.setHeader(Headers.ACCEPT_LANGUAGE, "en-US,en");
if (proxy != null) {
client.setProxyHandler(new ProxyHandler(switch (proxy.type()) {
case HTTP -> ProxyType.HTTP;
case SOCKS4 -> ProxyType.SOCKS4;
case SOCKS5 -> ProxyType.SOCKS5;
}, proxy.address(), proxy.username(), proxy.password()));
}
return client;
}
}

View file

@ -0,0 +1,64 @@
package org.geysermc.mcprotocollib.auth.util;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public class TextureUrlChecker {
private static final Set<String> ALLOWED_SCHEMES = Set.of(
"http",
"https"
);
private static final List<String> ALLOWED_DOMAINS = List.of(
".minecraft.net",
".mojang.com"
);
private static final List<String> BLOCKED_DOMAINS = List.of(
"bugs.mojang.com",
"education.minecraft.net",
"feedback.minecraft.net"
);
public static boolean isAllowedTextureDomain(final String url) {
final URI uri;
try {
uri = new URI(url).normalize();
} catch (final URISyntaxException ignored) {
return false;
}
final String scheme = uri.getScheme();
if (scheme == null || !ALLOWED_SCHEMES.contains(scheme)) {
return false;
}
final String domain = uri.getHost();
if (domain == null) {
return false;
}
final String decodedDomain = IDN.toUnicode(domain);
final String lowerCaseDomain = decodedDomain.toLowerCase(Locale.ROOT);
if (!lowerCaseDomain.equals(decodedDomain)) {
return false;
}
return isDomainOnList(decodedDomain, ALLOWED_DOMAINS) && !isDomainOnList(decodedDomain, BLOCKED_DOMAINS);
}
private static boolean isDomainOnList(final String domain, final List<String> list) {
for (final String entry : list) {
if (domain.endsWith(entry)) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,25 @@
package org.geysermc.mcprotocollib.auth.util;
import java.util.HexFormat;
import java.util.UUID;
public class UUIDUtils {
public static UUID convertToDashed(String noDashes) {
if (noDashes == null) {
return null;
}
return new UUID(
HexFormat.fromHexDigitsToLong(noDashes, 0, 16),
HexFormat.fromHexDigitsToLong(noDashes, 16, 32)
);
}
public static String convertToNoDashes(UUID uuid) {
if (uuid == null) {
return null;
}
return uuid.toString().replace("-", "");
}
}

View file

@ -0,0 +1,23 @@
package org.geysermc.mcprotocollib.auth.util;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.UUID;
/**
* Utility class for serializing and deserializing undashed UUIDs.
*/
public class UndashedUUIDAdapter extends TypeAdapter<UUID> {
@Override
public void write(JsonWriter out, UUID value) throws IOException {
out.value(UUIDUtils.convertToNoDashes(value));
}
@Override
public UUID read(JsonReader in) throws IOException {
return UUIDUtils.convertToDashed(in.nextString());
}
}

View file

@ -1,87 +1,37 @@
package org.geysermc.mcprotocollib.network;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
/**
* Information describing a network proxy.
*/
public class ProxyInfo {
private final Type type;
private final SocketAddress address;
private boolean authenticated;
private String username;
private String password;
public record ProxyInfo(Type type, SocketAddress address, String username, String password) {
/**
* Creates a new unauthenticated ProxyInfo instance.
* Creates a new unauthenticated proxy info.
*
* @param type Type of proxy.
*/
public ProxyInfo(Type type, String host, int port, String username, String password) {
this(type, new InetSocketAddress(host, port), username, password);
}
/**
* Creates a new unauthenticated proxy info.
*
* @param type Type of proxy.
* @param address Network address of the proxy.
*/
public ProxyInfo(Type type, SocketAddress address) {
this.type = type;
this.address = address;
this.authenticated = false;
this(type, address, null, null);
}
/**
* Creates a new authenticated ProxyInfo instance.
* Creates a new unauthenticated proxy info.
*
* @param type Type of proxy.
* @param address Network address of the proxy.
* @param username Username to authenticate with.
* @param password Password to authenticate with.
*/
public ProxyInfo(Type type, SocketAddress address, String username, String password) {
this(type, address);
this.authenticated = true;
this.username = username;
this.password = password;
}
/**
* Gets the proxy's type.
*
* @return The proxy's type.
*/
public Type getType() {
return this.type;
}
/**
* Gets the proxy's network address.
*
* @return The proxy's network address.
*/
public SocketAddress getAddress() {
return this.address;
}
/**
* Gets whether the proxy is authenticated with.
*
* @return Whether to authenticate with the proxy.
*/
public boolean isAuthenticated() {
return this.authenticated;
}
/**
* Gets the proxy's authentication username.
*
* @return The username to authenticate with.
*/
public String getUsername() {
return this.username;
}
/**
* Gets the proxy's authentication password.
*
* @return The password to authenticate with.
*/
public String getPassword() {
return this.password;
public ProxyInfo(Type type, String host, int port) {
this(type, new InetSocketAddress(host, port));
}
/**

View file

@ -28,8 +28,8 @@ import io.netty.handler.proxy.Socks5ProxyHandler;
import io.netty.resolver.dns.DnsNameResolver;
import io.netty.resolver.dns.DnsNameResolverBuilder;
import io.netty.util.concurrent.DefaultThreadFactory;
import org.geysermc.mcprotocollib.network.BuiltinFlags;
import org.geysermc.mcprotocollib.network.ProxyInfo;
import org.geysermc.mcprotocollib.network.BuiltinFlags;
import org.geysermc.mcprotocollib.network.codec.PacketCodecHelper;
import org.geysermc.mcprotocollib.network.helper.TransportHelper;
import org.geysermc.mcprotocollib.network.packet.PacketProtocol;
@ -207,29 +207,29 @@ public class TcpClientSession extends TcpSession {
private void addProxy(ChannelPipeline pipeline) {
if (proxy != null) {
switch (proxy.getType()) {
switch (proxy.type()) {
case HTTP -> {
if (proxy.isAuthenticated()) {
pipeline.addFirst("proxy", new HttpProxyHandler(proxy.getAddress(), proxy.getUsername(), proxy.getPassword()));
if (proxy.username() != null && proxy.password() != null) {
pipeline.addFirst("proxy", new HttpProxyHandler(proxy.address(), proxy.username(), proxy.password()));
} else {
pipeline.addFirst("proxy", new HttpProxyHandler(proxy.getAddress()));
pipeline.addFirst("proxy", new HttpProxyHandler(proxy.address()));
}
}
case SOCKS4 -> {
if (proxy.isAuthenticated()) {
pipeline.addFirst("proxy", new Socks4ProxyHandler(proxy.getAddress(), proxy.getUsername()));
if (proxy.username() != null) {
pipeline.addFirst("proxy", new Socks4ProxyHandler(proxy.address(), proxy.username()));
} else {
pipeline.addFirst("proxy", new Socks4ProxyHandler(proxy.getAddress()));
pipeline.addFirst("proxy", new Socks4ProxyHandler(proxy.address()));
}
}
case SOCKS5 -> {
if (proxy.isAuthenticated()) {
pipeline.addFirst("proxy", new Socks5ProxyHandler(proxy.getAddress(), proxy.getUsername(), proxy.getPassword()));
if (proxy.username() != null && proxy.password() != null) {
pipeline.addFirst("proxy", new Socks5ProxyHandler(proxy.address(), proxy.username(), proxy.password()));
} else {
pipeline.addFirst("proxy", new Socks5ProxyHandler(proxy.getAddress()));
pipeline.addFirst("proxy", new Socks5ProxyHandler(proxy.address()));
}
}
default -> throw new UnsupportedOperationException("Unsupported proxy type: " + proxy.getType());
default -> throw new UnsupportedOperationException("Unsupported proxy type: " + proxy.type());
}
}
}

View file

@ -1,13 +1,10 @@
package org.geysermc.mcprotocollib.protocol;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException;
import com.github.steveice10.mc.auth.exception.request.RequestException;
import com.github.steveice10.mc.auth.exception.request.ServiceUnavailableException;
import com.github.steveice10.mc.auth.service.SessionService;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.SneakyThrows;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.auth.SessionService;
import org.geysermc.mcprotocollib.network.Session;
import org.geysermc.mcprotocollib.network.event.session.ConnectedEvent;
import org.geysermc.mcprotocollib.network.event.session.SessionAdapter;
@ -44,6 +41,7 @@ import org.geysermc.mcprotocollib.protocol.packet.status.serverbound.Serverbound
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
@ -78,16 +76,10 @@ public class ClientListener extends SessionAdapter {
}
SessionService sessionService = session.getFlag(MinecraftConstants.SESSION_SERVICE_KEY, new SessionService());
String serverId = sessionService.getServerId(helloPacket.getServerId(), helloPacket.getPublicKey(), key);
String serverId = SessionService.getServerId(helloPacket.getServerId(), helloPacket.getPublicKey(), key);
try {
sessionService.joinServer(profile, accessToken, serverId);
} catch (ServiceUnavailableException e) {
session.disconnect("Login failed: Authentication service unavailable.", e);
return;
} catch (InvalidCredentialsException e) {
session.disconnect("Login failed: Invalid login session.", e);
return;
} catch (RequestException e) {
} catch (IOException e) {
session.disconnect("Login failed: Authentication error: " + e.getMessage(), e);
return;
}

View file

@ -1,7 +1,7 @@
package org.geysermc.mcprotocollib.protocol;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.auth.service.SessionService;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.auth.SessionService;
import org.geysermc.mcprotocollib.network.Flag;
import org.geysermc.mcprotocollib.network.packet.DefaultPacketHeader;
import org.geysermc.mcprotocollib.network.packet.PacketHeader;

View file

@ -1,6 +1,5 @@
package org.geysermc.mcprotocollib.protocol;
import com.github.steveice10.mc.auth.data.GameProfile;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.NonNull;
@ -8,6 +7,7 @@ import lombok.Setter;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtUtils;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.network.Server;
import org.geysermc.mcprotocollib.network.Session;
import org.geysermc.mcprotocollib.network.codec.PacketCodecHelper;

View file

@ -1,13 +1,12 @@
package org.geysermc.mcprotocollib.protocol;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.auth.exception.request.RequestException;
import com.github.steveice10.mc.auth.service.SessionService;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.auth.SessionService;
import org.geysermc.mcprotocollib.network.Session;
import org.geysermc.mcprotocollib.network.event.session.ConnectedEvent;
import org.geysermc.mcprotocollib.network.event.session.DisconnectingEvent;
@ -40,6 +39,7 @@ import org.geysermc.mcprotocollib.protocol.packet.status.serverbound.Serverbound
import org.geysermc.mcprotocollib.protocol.packet.status.serverbound.ServerboundStatusRequestPacket;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
@ -228,8 +228,8 @@ public class ServerListener extends SessionAdapter {
if (this.key != null) {
SessionService sessionService = this.session.getFlag(MinecraftConstants.SESSION_SERVICE_KEY, new SessionService());
try {
profile = sessionService.getProfileByServer(username, sessionService.getServerId(SERVER_ID, KEY_PAIR.getPublic(), this.key));
} catch (RequestException e) {
profile = sessionService.getProfileByServer(username, SessionService.getServerId(SERVER_ID, KEY_PAIR.getPublic(), this.key));
} catch (IOException e) {
this.session.disconnect("Failed to make session service request.", e);
return;
}

View file

@ -1,6 +1,5 @@
package org.geysermc.mcprotocollib.protocol.codec;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.google.gson.JsonElement;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
@ -18,6 +17,7 @@ import org.cloudburstmc.nbt.NBTInputStream;
import org.cloudburstmc.nbt.NBTOutputStream;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.network.codec.BasePacketCodecHelper;
import org.geysermc.mcprotocollib.protocol.data.DefaultComponentSerializer;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;

View file

@ -1,11 +1,11 @@
package org.geysermc.mcprotocollib.protocol.data.game;
import com.github.steveice10.mc.auth.data.GameProfile;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NonNull;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import java.security.PublicKey;

View file

@ -136,7 +136,7 @@ public enum EntityType {
@Getter
private final boolean projectile;
EntityType() {
this.projectile = false;
}

View file

@ -1,12 +1,12 @@
package org.geysermc.mcprotocollib.protocol.data.game.item.component;
import com.github.steveice10.mc.auth.data.GameProfile;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.nbt.NbtList;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;

View file

@ -1,6 +1,5 @@
package org.geysermc.mcprotocollib.protocol.data.game.item.component;
import com.github.steveice10.mc.auth.data.GameProfile;
import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
@ -9,6 +8,7 @@ import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.nbt.NbtList;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;

View file

@ -1,11 +1,11 @@
package org.geysermc.mcprotocollib.protocol.data.status;
import com.github.steveice10.mc.auth.data.GameProfile;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NonNull;
import lombok.Setter;
import org.geysermc.mcprotocollib.auth.GameProfile;
import java.util.List;

View file

@ -1,11 +1,11 @@
package org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound;
import com.github.steveice10.mc.auth.data.GameProfile;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.With;
import net.kyori.adventure.text.Component;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket;
import org.geysermc.mcprotocollib.protocol.data.game.PlayerListEntry;

View file

@ -1,11 +1,11 @@
package org.geysermc.mcprotocollib.protocol.packet.login.clientbound;
import com.github.steveice10.mc.auth.data.GameProfile;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NonNull;
import lombok.With;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket;

View file

@ -1,7 +1,5 @@
package org.geysermc.mcprotocollib.protocol.packet.status.clientbound;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.auth.util.Base64;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@ -12,6 +10,7 @@ import lombok.Data;
import lombok.NonNull;
import lombok.With;
import net.kyori.adventure.text.Component;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftPacket;
import org.geysermc.mcprotocollib.protocol.data.DefaultComponentSerializer;
@ -21,6 +20,7 @@ import org.geysermc.mcprotocollib.protocol.data.status.VersionInfo;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
@Data
@ -117,10 +117,10 @@ public class ClientboundStatusResponsePacket implements MinecraftPacket {
str = str.substring("data:image/png;base64,".length());
}
return Base64.decode(str.getBytes(StandardCharsets.UTF_8));
return Base64.getDecoder().decode(str.getBytes(StandardCharsets.UTF_8));
}
public static String iconToString(byte[] icon) {
return "data:image/png;base64," + new String(Base64.encode(icon), StandardCharsets.UTF_8);
return "data:image/png;base64," + new String(Base64.getEncoder().encode(icon), StandardCharsets.UTF_8);
}
}

View file

@ -1,6 +1,6 @@
package org.geysermc.mcprotocollib.protocol.packet.login.clientbound;
import com.github.steveice10.mc.auth.data.GameProfile;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.packet.PacketTest;
import org.junit.jupiter.api.BeforeEach;

View file

@ -1,7 +1,7 @@
package org.geysermc.mcprotocollib.protocol.packet.status.clientbound;
import com.github.steveice10.mc.auth.data.GameProfile;
import net.kyori.adventure.text.Component;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec;
import org.geysermc.mcprotocollib.protocol.data.status.PlayerInfo;
import org.geysermc.mcprotocollib.protocol.data.status.ServerStatusInfo;

View file

@ -17,4 +17,7 @@ dependencyResolutionManagement {
rootProject.name = "mcprotocollib"
include("protocol", "example")
include(
"protocol",
"example"
)