diff --git a/LaunchServer/source/auth/provider/AuthProvider.java b/LaunchServer/source/auth/provider/AuthProvider.java index 7e5e4f9..c4914ce 100644 --- a/LaunchServer/source/auth/provider/AuthProvider.java +++ b/LaunchServer/source/auth/provider/AuthProvider.java @@ -21,9 +21,9 @@ registerProvider("reject", RejectAuthProvider::new); registerProvider("delegate", DelegateAuthProvider::new); - // Auth providers that doesn't do nothing :D registerProvider("file", FileAuthProvider::new); registerProvider("mojang", MojangAuthProvider::new); + registerProvider("authlib", AuthlibAuthProvider::new); registerProvider("mysql", MySQLAuthProvider::new); registerProvider("mysql-bcrypt", MySQLBcryptAuthProvider::new); registerProvider("mysql-8", MySQL8AuthProvider::new); diff --git a/LaunchServer/source/auth/provider/AuthlibAuthProvider.java b/LaunchServer/source/auth/provider/AuthlibAuthProvider.java new file mode 100644 index 0000000..251ad43 --- /dev/null +++ b/LaunchServer/source/auth/provider/AuthlibAuthProvider.java @@ -0,0 +1,118 @@ +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 launcher.serialize.config.entry.StringConfigEntry; + +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 AuthlibAuthProvider 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; + private static String authUrl; + + // TODO: + // https://wiki.vg/Authentication#Refresh + // https://wiki.vg/Authentication#Signout + static + { + try + { + // Docs: https://wiki.vg/Authentication#Authenticate + URL = new URL(authUrl); // "https://authserver.mojang.com/authenticate" + } + catch (MalformedURLException e) + { + throw new InternalError(e); + } + } + + AuthlibAuthProvider(BlockConfigEntry block) + { + super(block); + authUrl = block.getEntryValue("authUrl", StringConfigEntry.class); + } + + public static JsonObject 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)); + } + } + connection.getResponseCode(); // Actually make request + + // 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 Mojang response: '" + json + '\''); + 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 = makeAuthlibRequest(URL, request); + if (response == null) + { + authError("Empty authlib 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("clientToken").asString(); + UUID uuid = UUID.fromString(UUID_REGEX.matcher(selectedProfile.get("id").asString()).replaceFirst("$1-$2-$3-$4-$5")); + String launcherToken = response.get("accessToken").asString(); + + // We're done + return new AuthlibAuthProviderResult(username, accessToken, uuid, launcherToken); + } + + @Override + public void close() + { + // Do nothing + } +} diff --git a/LaunchServer/source/auth/provider/AuthlibAuthProviderResult.java b/LaunchServer/source/auth/provider/AuthlibAuthProviderResult.java new file mode 100644 index 0000000..2636fb7 --- /dev/null +++ b/LaunchServer/source/auth/provider/AuthlibAuthProviderResult.java @@ -0,0 +1,16 @@ +package launchserver.auth.provider; + +import java.util.UUID; + +public class AuthlibAuthProviderResult extends AuthProviderResult +{ + public final UUID uuid; + public final String launcherToken; + + AuthlibAuthProviderResult(String username, String accessToken, UUID uuid, String launcherToken) + { + super(username, accessToken); + this.uuid = uuid; + this.launcherToken = launcherToken; + } +} diff --git a/LaunchServer/source/texture/AuthlibTextureProvider.java b/LaunchServer/source/texture/AuthlibTextureProvider.java new file mode 100644 index 0000000..ff17e62 --- /dev/null +++ b/LaunchServer/source/texture/AuthlibTextureProvider.java @@ -0,0 +1,180 @@ +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.Texture; +import launcher.helper.IOHelper; +import launcher.helper.JVMHelper; +import launcher.helper.LogHelper; +import launcher.helper.VerifyHelper; +import launcher.serialize.config.entry.BlockConfigEntry; +import launcher.serialize.config.entry.StringConfigEntry; +import launchserver.auth.provider.MojangAuthProvider; + +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 AuthlibTextureProvider extends TextureProvider +{ + // Instance + private final String setUuidURL; + private final String setProfileURL; + + @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 AuthlibTextureProvider(BlockConfigEntry block) + { + super(block); + + // "https://api.mojang.com/users/profiles/minecraft/" + setUuidURL = block.getEntryValue("usersURL", StringConfigEntry.class); + // "https://sessionserver.mojang.com/session/minecraft/profile/" + setProfileURL = block.getEntryValue("profileURL", StringConfigEntry.class); + + // TODO: Verify + //IOHelper.verifyURL(setUuidURL); + //IOHelper.verifyURL(setProfileURL); + } + + @Override + public void close() + { + // Do nothing + } + + @Override + public synchronized Texture getCloakTexture(UUID uuid, String username) + { + return getCached(uuid, username).skin; + } + + @Override + public synchronized Texture getSkinTexture(UUID uuid, String username) + { + return getCached(uuid, username).cloak; + } + + private CacheData getCached(UUID uuid, String username) + { + 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 + { + // TODO Don't query UUID by username if using mojang auth handler (not implemented yet) + URL uuidURL = new URL(setUuidURL + IOHelper.urlEncode(username)); + JsonObject uuidResponse = MojangAuthProvider.makeMojangRequest(uuidURL, null); + if (uuidResponse == null) + { + throw new IllegalArgumentException("Empty UUID response"); + } + String uuidResolved = uuidResponse.get("id").asString(); + + // Obtain player profile + URL profileURL = new URL(setProfileURL + uuidResolved); + JsonObject profileResponse = MojangAuthProvider.makeMojangRequest(profileURL, null); + if (profileResponse == null) + { + throw new IllegalArgumentException("Empty Authlib response"); + } + JsonArray properties = (JsonArray) profileResponse.get("properties"); + if (properties == null) + { + LogHelper.subDebug("No 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("No textures property"); + return cache(username, null, null, null); + } + + // Extract skin&cloak texture + texturesProperty = (JsonObject) texturesProperty.get("textures"); + JsonObject skinProperty = (JsonObject) texturesProperty.get("SKIN"); + Texture skinTexture = skinProperty == null ? null : new Texture(skinProperty.get("url").asString(), false); + JsonObject cloakProperty = (JsonObject) texturesProperty.get("CAPE"); + Texture cloakTexture = cloakProperty == null ? null : new 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 CacheData cache(String username, Texture skin, Texture cloak, Throwable exc) + { + long until = CACHE_DURATION_MS == 0L ? Long.MIN_VALUE : System.currentTimeMillis() + CACHE_DURATION_MS; + CacheData data = exc == null ? new CacheData(skin, cloak, until) : new CacheData(exc, until); + if (CACHE_DURATION_MS != 0L) + { + cache.put(username, data); + } + return data; + } + + private static final class CacheData + { + private final Texture skin, cloak; + private final Throwable exc; + private final long until; + + private CacheData(Texture skin, 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 e6a3c0b..97c344b 100644 --- a/LaunchServer/source/texture/TextureProvider.java +++ b/LaunchServer/source/texture/TextureProvider.java @@ -20,6 +20,7 @@ { registerProvider("void", VoidTextureProvider::new); registerProvider("delegate", DelegateTextureProvider::new); + registerProvider("authlib", AuthlibTextureProvider::new); // Auth providers that doesn't do nothing :D registerProvider("mojang", MojangTextureProvider::new); diff --git a/buildnumber b/buildnumber index da922d9..fd010a6 100644 --- a/buildnumber +++ b/buildnumber @@ -1 +1 @@ -539, 30.04.2021 \ No newline at end of file +540, 30.04.2021 \ No newline at end of file