diff --git a/LaunchServer/source/auth/handler/AuthHandler.java b/LaunchServer/source/auth/handler/AuthHandler.java index 09b7860..9749f65 100644 --- a/LaunchServer/source/auth/handler/AuthHandler.java +++ b/LaunchServer/source/auth/handler/AuthHandler.java @@ -21,6 +21,9 @@ { registerHandler("memory", MemoryAuthHandler::new); registerHandler("delegate", DelegateAuthHandler::new); + registerHandler("mojang", MojangAuthHandler::new); + registerHandler("authlib", AuthlibAuthHandler::new); + registerHandler("minesocial", MineSocialAuthHandler::new); // Auth handler that doesn't do nothing :D registerHandler("binaryFile", BinaryFileAuthHandler::new); diff --git a/LaunchServer/source/auth/handler/AuthlibAuthHandler.java b/LaunchServer/source/auth/handler/AuthlibAuthHandler.java new file mode 100644 index 0000000..bf750c1 --- /dev/null +++ b/LaunchServer/source/auth/handler/AuthlibAuthHandler.java @@ -0,0 +1,117 @@ +package launchserver.auth.handler; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.WriterConfig; +import launcher.helper.IOHelper; +import launcher.helper.LogHelper; +import launcher.serialize.config.entry.BlockConfigEntry; +import launcher.serialize.config.entry.StringConfigEntry; +import launchserver.auth.provider.AuthProviderResult; +import launchserver.auth.provider.AuthlibAuthProviderResult; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class AuthlibAuthHandler extends AuthHandler +{ + private static java.net.URL URL; + private static String joinUrl; + + public final HashMap usernameToUUID = new HashMap<>(); + + AuthlibAuthHandler(BlockConfigEntry block) + { + super(block); + joinUrl = block.getEntryValue("joinUrl", StringConfigEntry.class); + + try + { + // Docs: https://wiki.vg/Protocol_Encryption#Client + URL = new URL(joinUrl); // "https://sessionserver.mojang.com/session/minecraft/join" + } + catch (MalformedURLException e) + { + throw new InternalError(e); + } + } + + @Override + public UUID auth(AuthProviderResult authResult) { + if (authResult instanceof AuthlibAuthProviderResult) { + AuthlibAuthProviderResult result = (AuthlibAuthProviderResult) authResult; + usernameToUUID.put(result.username, result.uuid); + return result.uuid; + } + return null; + } + + @Override + public UUID checkServer(String username, String serverID) { + return UUID.fromString(serverID); + } + + @Override + public void close() { + } + + @Override + public boolean joinServer(String username, String accessToken, String serverID) throws IOException { + JsonObject request = Json.object(). + add("agent", Json.object().add("name", "Minecraft").add("version", 1)). + add("accessToken", accessToken).add("selectedProfile", usernameToUUID(username).toString().replace("-", "")). + add("serverId", serverID); + + int response = makeAuthlibRequest(URL, request); + + if (200 <= response && response < 300 ) + { + return true; + } + else + { + authError("Empty Authlib Handler response"); + } + return false; + } + + public static int makeAuthlibRequest(URL url, JsonObject request) throws IOException + { + HttpURLConnection connection = request == null ? + (HttpURLConnection) IOHelper.newConnection(url) : + IOHelper.newConnectionPost(url); + + // Make request + if (request != null) + { + connection.setRequestProperty("Content-Type", "application/json"); + try (OutputStream output = connection.getOutputStream()) + { + output.write(request.toString(WriterConfig.MINIMAL).getBytes(StandardCharsets.UTF_8)); + } + } + int statusCode = connection.getResponseCode(); + LogHelper.subDebug("Raw Authlib status сode: '" + statusCode + '\''); + return statusCode; + } + + @Override + public UUID usernameToUUID(String username) { + return usernameToUUID.get(username); + } + + @Override + public String uuidToUsername(UUID uuid) { + for (Map.Entry entry : usernameToUUID.entrySet()) { + if (entry.getValue().equals(uuid)) return entry.getKey(); + } + return null; + } +} diff --git a/LaunchServer/source/auth/handler/MineSocialAuthHandler.java b/LaunchServer/source/auth/handler/MineSocialAuthHandler.java new file mode 100644 index 0000000..0688257 --- /dev/null +++ b/LaunchServer/source/auth/handler/MineSocialAuthHandler.java @@ -0,0 +1,116 @@ +package launchserver.auth.handler; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.WriterConfig; +import launcher.helper.IOHelper; +import launcher.helper.LogHelper; +import launcher.serialize.config.entry.BlockConfigEntry; +import launchserver.auth.provider.AuthProviderResult; +import launchserver.auth.provider.MineSocialAuthProviderResult; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class MineSocialAuthHandler extends AuthHandler +{ + private static final java.net.URL URL; + + static + { + try + { + URL = new URL("https://sessionserver.minesocial.net/session/minecraft/join"); + } + catch (MalformedURLException e) + { + throw new InternalError(e); + } + } + + public final HashMap usernameToUUID = new HashMap<>(); + + MineSocialAuthHandler(BlockConfigEntry block) + { + super(block); + } + + @Override + public UUID auth(AuthProviderResult authResult) { + if (authResult instanceof MineSocialAuthProviderResult) { + MineSocialAuthProviderResult result = (MineSocialAuthProviderResult) authResult; + usernameToUUID.put(result.username, result.uuid); + return result.uuid; + } + return null; + } + + @Override + public UUID checkServer(String username, String serverID) { + return UUID.fromString(serverID); + } + + @Override + public void close() { + } + + @Override + public boolean joinServer(String username, String accessToken, String serverID) throws IOException { + JsonObject request = Json.object(). + add("agent", Json.object().add("name", "Minecraft").add("version", 1)). + add("accessToken", accessToken).add("selectedProfile", usernameToUUID(username).toString().replace("-", "")). + add("serverId", serverID); + + int response = makeMineSocialRequest(URL, request); + + if (200 <= response && response < 300 ) + { + return true; + } + else + { + authError("Empty MineSocial Handler response"); + } + return false; + } + + public static int makeMineSocialRequest(URL url, JsonObject request) throws IOException + { + HttpURLConnection connection = request == null ? + (HttpURLConnection) IOHelper.newConnection(url) : + IOHelper.newConnectionPost(url); + + // Make request + if (request != null) + { + connection.setRequestProperty("Content-Type", "application/json"); + try (OutputStream output = connection.getOutputStream()) + { + output.write(request.toString(WriterConfig.MINIMAL).getBytes(StandardCharsets.UTF_8)); + } + } + int statusCode = connection.getResponseCode(); + LogHelper.subDebug("Raw MineSocial status сode: '" + statusCode + '\''); + return statusCode; + } + + @Override + public UUID usernameToUUID(String username) { + return usernameToUUID.get(username); + } + + @Override + public String uuidToUsername(UUID uuid) { + for (Map.Entry entry : usernameToUUID.entrySet()) { + if (entry.getValue().equals(uuid)) return entry.getKey(); + } + return null; + } +} diff --git a/LaunchServer/source/auth/handler/MojangAuthHandler.java b/LaunchServer/source/auth/handler/MojangAuthHandler.java new file mode 100644 index 0000000..ed042d0 --- /dev/null +++ b/LaunchServer/source/auth/handler/MojangAuthHandler.java @@ -0,0 +1,118 @@ +package launchserver.auth.handler; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.WriterConfig; +import launcher.helper.IOHelper; +import launcher.helper.LogHelper; +import launcher.serialize.config.entry.BlockConfigEntry; +import launchserver.auth.provider.AuthProviderResult; +import launchserver.auth.provider.MojangAuthProviderResult; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class MojangAuthHandler extends AuthHandler +{ + private static final java.net.URL URL; + + static + { + try + { + URL = new URL("https://sessionserver.mojang.com/session/minecraft/join"); + } + catch (MalformedURLException e) + { + throw new InternalError(e); + } + } + + public final HashMap usernameToUUID = new HashMap<>(); + + MojangAuthHandler(BlockConfigEntry block) + { + super(block); + } + + @Override + public UUID auth(AuthProviderResult authResult) { + if (authResult instanceof MojangAuthProviderResult) { + MojangAuthProviderResult result = (MojangAuthProviderResult) authResult; + usernameToUUID.put(result.username, result.uuid); + return result.uuid; + } + return null; + } + + @Override + public UUID checkServer(String username, String serverID) { + // .....допустим + return UUID.fromString(serverID); + } + + @Override + public void close() { + } + + @Override + public boolean joinServer(String username, String accessToken, String serverID) throws IOException { + JsonObject request = Json.object(). + add("agent", Json.object().add("name", "Minecraft").add("version", 1)). + add("accessToken", accessToken).add("selectedProfile", usernameToUUID(username).toString().replace("-", "")). + add("serverId", serverID); + + int response = makeMojangRequest(URL, request); + + if (200 <= response && response < 300 ) + { + return true; + } + else + { + authError("Empty Mojang Handler response"); + } + return false; + } + + // TODO: Я потом как нидь сделаю рефактор на эту тему + public static int makeMojangRequest(URL url, JsonObject request) throws IOException + { + HttpURLConnection connection = request == null ? + (HttpURLConnection) IOHelper.newConnection(url) : + IOHelper.newConnectionPost(url); + + // Make request + if (request != null) + { + connection.setRequestProperty("Content-Type", "application/json"); + try (OutputStream output = connection.getOutputStream()) + { + output.write(request.toString(WriterConfig.MINIMAL).getBytes(StandardCharsets.UTF_8)); + } + } + int statusCode = connection.getResponseCode(); + LogHelper.subDebug("Raw Mojang status сode: '" + statusCode + '\''); + return statusCode; // https://wiki.vg/Protocol_Encryption#Client + } + + @Override + public UUID usernameToUUID(String username) { + return usernameToUUID.get(username); + } + + @Override + public String uuidToUsername(UUID uuid) { + for (Map.Entry entry : usernameToUUID.entrySet()) { + if (entry.getValue().equals(uuid)) return entry.getKey(); + } + return null; + } +} diff --git a/LaunchServer/source/auth/provider/AuthProvider.java b/LaunchServer/source/auth/provider/AuthProvider.java index c4914ce..831ca36 100644 --- a/LaunchServer/source/auth/provider/AuthProvider.java +++ b/LaunchServer/source/auth/provider/AuthProvider.java @@ -24,6 +24,7 @@ registerProvider("file", FileAuthProvider::new); registerProvider("mojang", MojangAuthProvider::new); registerProvider("authlib", AuthlibAuthProvider::new); + registerProvider("minesocial", MineSocialAuthProvider::new); registerProvider("mysql", MySQLAuthProvider::new); registerProvider("mysql-bcrypt", MySQLBcryptAuthProvider::new); registerProvider("mysql-8", MySQL8AuthProvider::new); diff --git a/LaunchServer/source/auth/provider/MineSocialAuthProvider.java b/LaunchServer/source/auth/provider/MineSocialAuthProvider.java new file mode 100644 index 0000000..0aea68c --- /dev/null +++ b/LaunchServer/source/auth/provider/MineSocialAuthProvider.java @@ -0,0 +1,119 @@ +package launchserver.auth.provider; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; +import com.eclipsesource.json.WriterConfig; +import launcher.helper.IOHelper; +import launcher.helper.LogHelper; +import launcher.serialize.config.entry.BlockConfigEntry; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.regex.Pattern; + +public class MineSocialAuthProvider extends AuthProvider +{ + private static final Pattern UUID_REGEX = Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})"); + private static final java.net.URL URL; + + static + { + try + { + URL = new URL("https://authserver.minesocial.net/authenticate"); + } + catch (MalformedURLException e) + { + throw new InternalError(e); + } + } + + MineSocialAuthProvider(BlockConfigEntry block) + { + super(block); + } + + public static JsonObject makeMineSocialRequest(URL url, JsonObject request) throws IOException + { + HttpURLConnection connection = request == null ? + (HttpURLConnection) IOHelper.newConnection(url) : + IOHelper.newConnectionPost(url); + + // Make request + if (request != null) + { + connection.setRequestProperty("Content-Type", "application/json"); + try (OutputStream output = connection.getOutputStream()) + { + output.write(request.toString(WriterConfig.MINIMAL).getBytes(StandardCharsets.UTF_8)); + } + } + int statusCode = connection.getResponseCode(); + + // Read response + InputStream errorInput = connection.getErrorStream(); + try (InputStream input = errorInput == null ? connection.getInputStream() : errorInput) + { + String charset = connection.getContentEncoding(); + Charset charsetObject = charset == null ? + IOHelper.UNICODE_CHARSET : Charset.forName(charset); + + // Parse response + String json = new String(IOHelper.read(input), charsetObject); + LogHelper.subDebug("Raw MineSocial response: '" + json + '\''); + + if (200 <= statusCode && statusCode < 300) + { + return Json.parse(json).asObject(); + } + else + { + return json.isEmpty() ? null : Json.parse(json).asObject(); + } + } + } + + @Override + public AuthProviderResult auth(String login, String password, String ip) throws Throwable + { + JsonObject request = Json.object(). + add("agent", Json.object().add("name", "Minecraft").add("version", 1)). + add("username", login).add("password", password); + + // Verify there's no error + JsonObject response = makeMineSocialRequest(URL, request); + if (response == null) + { + authError("Empty MineSocial Provider response"); + } + JsonValue errorMessage = response.get("errorMessage"); + if (errorMessage != null) + { + authError(errorMessage.asString()); + } + + // Parse JSON data + JsonObject selectedProfile = response.get("selectedProfile").asObject(); + String username = selectedProfile.get("name").asString(); + String accessToken = response.get("accessToken").asString(); + UUID uuid = UUID.fromString(UUID_REGEX.matcher(selectedProfile.get("id").asString()).replaceFirst("$1-$2-$3-$4-$5")); + String launcherToken = response.get("clientToken").asString(); + + // We're done + return new MineSocialAuthProviderResult(username, accessToken, uuid, launcherToken); + } + + @Override + public void close() + { + // Do nothing + } +} diff --git a/LaunchServer/source/auth/provider/MineSocialAuthProviderResult.java b/LaunchServer/source/auth/provider/MineSocialAuthProviderResult.java new file mode 100644 index 0000000..70d7c63 --- /dev/null +++ b/LaunchServer/source/auth/provider/MineSocialAuthProviderResult.java @@ -0,0 +1,16 @@ +package launchserver.auth.provider; + +import java.util.UUID; + +public class MineSocialAuthProviderResult extends AuthProviderResult +{ + public final UUID uuid; + public final String launcherToken; + + MineSocialAuthProviderResult(String username, String accessToken, UUID uuid, String launcherToken) + { + super(username, accessToken); + this.uuid = uuid; + this.launcherToken = launcherToken; + } +} diff --git a/LaunchServer/source/texture/MineSocialTextureProvider.java b/LaunchServer/source/texture/MineSocialTextureProvider.java new file mode 100644 index 0000000..7e40536 --- /dev/null +++ b/LaunchServer/source/texture/MineSocialTextureProvider.java @@ -0,0 +1,167 @@ +package launchserver.texture; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; +import launcher.LauncherAPI; +import launcher.client.PlayerProfile; +import launcher.helper.IOHelper; +import launcher.helper.JVMHelper; +import launcher.helper.LogHelper; +import launcher.helper.VerifyHelper; +import launcher.serialize.config.entry.BlockConfigEntry; +import launchserver.auth.provider.MineSocialAuthProvider; + +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class MineSocialTextureProvider extends TextureProvider +{ + @LauncherAPI + public static final long CACHE_DURATION_MS = VerifyHelper.verifyLong( + Long.parseLong( + System.getProperty("launcher.mysql.cacheDurationHours", Integer.toString(24)) + ), + VerifyHelper.L_NOT_NEGATIVE, "launcher.mysql.cacheDurationHours can't be < 0") * 60L * 60L * 1000L; + + // Instance + private final Map cache = new HashMap<>(1024); + + public MineSocialTextureProvider(BlockConfigEntry block) + { + super(block); + } + + @Override + public void close() + { + // Do nothing + } + + @Override + public synchronized PlayerProfile.Texture getCloakTexture(UUID uuid, String username) + { + return getCached(uuid, username).cloak; + } + + @Override + public synchronized PlayerProfile.Texture getSkinTexture(UUID uuid, String username) + { + return getCached(uuid, username).skin; + } + + private MineSocialTextureProvider.CacheData getCached(UUID uuid, String username) + { + MineSocialTextureProvider.CacheData result = cache.get(username); + + // Have cached result? + if (result != null && System.currentTimeMillis() < result.until) + { + if (result.exc != null) + { + JVMHelper.UNSAFE.throwException(result.exc); + } + return result; + } + + try + { + URL uuidURL = new URL("https://api.minesocial.net/users/profiles/minecraft/" + IOHelper.urlEncode(username)); + JsonObject uuidResponse = MineSocialAuthProvider.makeMineSocialRequest(uuidURL, null); + if (uuidResponse == null) + { + throw new IllegalArgumentException("Empty MineSocial UUID response!"); + } + String uuidResolved = uuidResponse.get("id").asString(); + + // Obtain player profile + URL profileURL = new URL("https://sessionserver.minesocial.net/session/minecraft/profile/" + uuidResolved); + JsonObject profileResponse = MineSocialAuthProvider.makeMineSocialRequest(profileURL, null); + if (profileResponse == null) + { + throw new IllegalArgumentException("Empty MineSocial profile response!"); + } + JsonArray properties = (JsonArray) profileResponse.get("properties"); + if (properties == null) + { + LogHelper.subDebug("Not get MineSocial properties!"); + return cache(username, null, null, null); + } + + // Find textures property + JsonObject texturesProperty = null; + for (JsonValue property : properties) + { + JsonObject property0 = property.asObject(); + if (property0.get("name").asString().equals("textures")) + { + byte[] asBytes = Base64.getDecoder().decode(property0.get("value").asString()); + String asString = new String(asBytes, StandardCharsets.UTF_8); + texturesProperty = Json.parse(asString).asObject(); + break; + } + } + if (texturesProperty == null) + { + LogHelper.subDebug("Not get MineSocial textures property!"); + return cache(username, null, null, null); + } + + // Extract skin&cloak texture + texturesProperty = (JsonObject) texturesProperty.get("textures"); + JsonObject skinProperty = (JsonObject) texturesProperty.get("SKIN"); + PlayerProfile.Texture skinTexture = skinProperty == null ? null : new PlayerProfile.Texture(skinProperty.get("url").asString(), false); + JsonObject cloakProperty = (JsonObject) texturesProperty.get("CAPE"); + PlayerProfile.Texture cloakTexture = cloakProperty == null ? null : new PlayerProfile.Texture(cloakProperty.get("url").asString(), true); + + // We're done + return cache(username, skinTexture, cloakTexture, null); + } + catch (Throwable exc) + { + cache(username, null, null, exc); + JVMHelper.UNSAFE.throwException(exc); + } + + // We're dones + return result; + } + + private MineSocialTextureProvider.CacheData cache(String username, PlayerProfile.Texture skin, PlayerProfile.Texture cloak, Throwable exc) + { + long until = CACHE_DURATION_MS == 0L ? Long.MIN_VALUE : System.currentTimeMillis() + CACHE_DURATION_MS; + MineSocialTextureProvider.CacheData data = exc == null ? new MineSocialTextureProvider.CacheData(skin, cloak, until) : new MineSocialTextureProvider.CacheData(exc, until); + if (CACHE_DURATION_MS != 0L) + { + cache.put(username, data); + } + return data; + } + + private static final class CacheData + { + private final PlayerProfile.Texture skin, cloak; + private final Throwable exc; + private final long until; + + private CacheData(PlayerProfile.Texture skin, PlayerProfile.Texture cloak, long until) + { + this.skin = skin; + this.cloak = cloak; + this.until = until; + exc = null; + } + + private CacheData(Throwable exc, long until) + { + this.exc = exc; + this.until = until; + skin = cloak = null; + } + } +} diff --git a/LaunchServer/source/texture/TextureProvider.java b/LaunchServer/source/texture/TextureProvider.java index 97c344b..12ae44f 100644 --- a/LaunchServer/source/texture/TextureProvider.java +++ b/LaunchServer/source/texture/TextureProvider.java @@ -21,6 +21,7 @@ registerProvider("void", VoidTextureProvider::new); registerProvider("delegate", DelegateTextureProvider::new); registerProvider("authlib", AuthlibTextureProvider::new); + registerProvider("minesocial", MineSocialTextureProvider::new); // Auth providers that doesn't do nothing :D registerProvider("mojang", MojangTextureProvider::new); diff --git a/Launcher/source/Launcher.java b/Launcher/source/Launcher.java index cffbf4e..540137b 100644 --- a/Launcher/source/Launcher.java +++ b/Launcher/source/Launcher.java @@ -59,7 +59,7 @@ { // Version info @LauncherAPI - public static final String VERSION = "1.7.3.0"; + public static final String VERSION = "1.7.3.3"; @LauncherAPI public static final String BUILD = readBuildNumber(); @LauncherAPI diff --git a/buildnumber b/buildnumber index d51f332..614e736 100644 --- a/buildnumber +++ b/buildnumber @@ -1 +1 @@ -547, 02.05.2021 \ No newline at end of file +550, 01.06.2021 \ No newline at end of file