diff --git a/.gitignore b/.gitignore index 04842cd..1810748 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ Launcher.jar LauncherAuthlib.jar -LaunchServer.jar \ No newline at end of file +LaunchServer.jar diff --git a/LaunchServer/source/LaunchServer.java b/LaunchServer/source/LaunchServer.java index fd7cb46..2c6d6b0 100644 --- a/LaunchServer/source/LaunchServer.java +++ b/LaunchServer/source/LaunchServer.java @@ -6,6 +6,7 @@ import javax.script.ScriptException; import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -37,6 +38,7 @@ import launcher.LauncherAPI; import launcher.client.ClientLauncher; import launcher.client.ClientProfile; +import launcher.client.PlayerProfile; import launcher.hasher.HashedDir; import launcher.helper.CommonHelper; import launcher.helper.IOHelper; @@ -412,6 +414,10 @@ private final StringConfigEntry address; private final String bindAddress; + // Skin system + @LauncherAPI public final String skinsURL; + @LauncherAPI public final String cloaksURL; + // Auth @LauncherAPI public final AuthHandler authHandler; @LauncherAPI public final AuthProvider authProvider; @@ -419,10 +425,6 @@ // EXE binary building @LauncherAPI public final boolean launch4J; - // Skin system - private final String skinsURL; - private final String cloaksURL; - private Config(BlockConfigEntry block) { super(block); address = block.getEntry("address", StringConfigEntry.class); @@ -433,12 +435,12 @@ // Skin system skinsURL = block.getEntryValue("skinsURL", StringConfigEntry.class); - String skinURL = getSkinURL("skinUsername", ZERO_UUID); + String skinURL = getTextureURL(skinsURL, "skinUsername", ZERO_UUID); if (skinURL != null) { IOHelper.verifyURL(skinURL); } cloaksURL = block.getEntryValue("cloaksURL", StringConfigEntry.class); - String cloakURL = getCloakURL("cloakUsername", ZERO_UUID); + String cloakURL = getTextureURL(cloaksURL, "cloakUsername", ZERO_UUID); if (cloakURL != null) { IOHelper.verifyURL(cloakURL); } @@ -464,13 +466,13 @@ } @LauncherAPI - public String getCloakURL(String username, UUID uuid) { - return getTextureURL(cloaksURL, username, uuid); + public PlayerProfile.Texture getCloak(String username, UUID uuid) { + return getTexture(getTextureURL(cloaksURL, username, uuid)); } @LauncherAPI - public String getSkinURL(String username, UUID uuid) { - return getTextureURL(skinsURL, username, uuid); + public PlayerProfile.Texture getSkin(String username, UUID uuid) { + return getTexture(getTextureURL(skinsURL, username, uuid)); } @LauncherAPI @@ -489,6 +491,18 @@ } @LauncherAPI + public static PlayerProfile.Texture getTexture(String url) { + try { + return new PlayerProfile.Texture(url); + } catch (FileNotFoundException e) { + return null; + } catch (IOException e) { + LogHelper.error(new IOException(String.format("Can't digest texture: '%s'", url), e)); + return null; + } + } + + @LauncherAPI public static String getTextureURL(String url, String username, UUID uuid) { if (url.isEmpty()) { return null; diff --git a/LaunchServer/source/auth/MySQLSourceConfig.java b/LaunchServer/source/auth/MySQLSourceConfig.java index 6b435df..a994af1 100644 --- a/LaunchServer/source/auth/MySQLSourceConfig.java +++ b/LaunchServer/source/auth/MySQLSourceConfig.java @@ -18,10 +18,10 @@ public final class MySQLSourceConfig extends ConfigObject implements Flushable { @LauncherAPI public static final int TIMEOUT = VerifyHelper.verifyInt( - Integer.parseUnsignedInt(System.getProperty("launcher.mysql.timeout", Integer.toString(10))), + Integer.parseUnsignedInt(System.getProperty("launcher.mysql.timeout", Integer.toString(5))), VerifyHelper.POSITIVE, "launcher.mysql.timeout can't be <= 0"); private static final int MAX_POOL_SIZE = VerifyHelper.verifyInt( - Integer.parseUnsignedInt(System.getProperty("launcher.mysql.maxPoolSize", Integer.toString(10))), + Integer.parseUnsignedInt(System.getProperty("launcher.mysql.maxPoolSize", Integer.toString(25))), VerifyHelper.POSITIVE, "launcher.mysql.maxPoolSize can't be <= 0"); private static final int STMT_CACHE_SIZE = VerifyHelper.verifyInt( Integer.parseUnsignedInt(System.getProperty("launcher.mysql.stmtCacheSize", Integer.toString(250))), diff --git a/LaunchServer/source/command/hash/IndexAssetCommand.java b/LaunchServer/source/command/hash/IndexAssetCommand.java index 6561c21..13d1316 100644 --- a/LaunchServer/source/command/hash/IndexAssetCommand.java +++ b/LaunchServer/source/command/hash/IndexAssetCommand.java @@ -97,17 +97,17 @@ String name = IOHelper.toString(inputAssetDir.relativize(file)); LogHelper.subInfo("Indexing: '%s'", name); - // Calculate SHA-1 hash sum and get size - String hash = SecurityHelper.toHex(SecurityHelper.digest(SecurityHelper.DigestAlgorithm.SHA1, file)); + // Calculate SHA-1 digest and get size + String digest = SecurityHelper.toHex(SecurityHelper.digest(SecurityHelper.DigestAlgorithm.SHA1, file)); // Add to objects JSONObject object = new JSONObject(); object.put("size", attrs.size()); - object.put("hash", hash); + object.put("hash", digest); objects.put(name, object); // Copy file - IOHelper.copy(file, resolveObjectFile(outputAssetDir, hash)); + IOHelper.copy(file, resolveObjectFile(outputAssetDir, digest)); return super.visitFile(file, attrs); } } diff --git a/LaunchServer/source/response/profile/ProfileByUUIDResponse.java b/LaunchServer/source/response/profile/ProfileByUUIDResponse.java index 8a58a98..8a4864f 100644 --- a/LaunchServer/source/response/profile/ProfileByUUIDResponse.java +++ b/LaunchServer/source/response/profile/ProfileByUUIDResponse.java @@ -32,8 +32,8 @@ } public static PlayerProfile getProfile(LaunchServer server, UUID uuid, String username) { - String skinURL = server.config.getSkinURL(username, uuid); - String cloakURL = server.config.getCloakURL(username, uuid); + PlayerProfile.Texture skinURL = server.config.getSkin(username, uuid); + PlayerProfile.Texture cloakURL = server.config.getCloak(username, uuid); return new PlayerProfile(uuid, username, skinURL, cloakURL); } } diff --git a/Launcher/source/client/ClientLauncher.java b/Launcher/source/client/ClientLauncher.java index 22099e8..1767ab9 100644 --- a/Launcher/source/client/ClientLauncher.java +++ b/Launcher/source/client/ClientLauncher.java @@ -55,8 +55,10 @@ private static final Pattern UUID_PATTERN = Pattern.compile("-", Pattern.LITERAL); // Authlib constants - @LauncherAPI public static final String SKIN_URL_PROPERTY = "l_skinURL"; - @LauncherAPI public static final String CLOAK_URL_PROPERTY = "l_cloakURL"; + @LauncherAPI public static final String SKIN_URL_PROPERTY = "skinURL"; + @LauncherAPI public static final String SKIN_DIGEST_PROPERTY = "skinDigest"; + @LauncherAPI public static final String CLOAK_URL_PROPERTY = "cloakURL"; + @LauncherAPI public static final String CLOAK_DIGEST_PROPERTY = "cloakDigest"; // Used to determine from clientside is launched from launcher private static final AtomicBoolean LAUNCHED = new AtomicBoolean(false); @@ -197,11 +199,15 @@ // Add user properties Collections.addAll(args, "--userType", "mojang"); JSONObject properties = new JSONObject(); - if (pp.skinURL != null) { - properties.put(SKIN_URL_PROPERTY, new JSONArray(Collections.singleton(pp.skinURL))); + if (pp.skin != null) { + properties.put(SKIN_URL_PROPERTY, new JSONArray(Collections.singleton(pp.skin.url))); + properties.put(SKIN_DIGEST_PROPERTY, new JSONArray(Collections.singleton( + SecurityHelper.toHex(pp.skin.digest)))); } - if (pp.cloakURL != null) { - properties.put(CLOAK_URL_PROPERTY, new JSONArray(Collections.singleton(pp.cloakURL))); + if (pp.skin != null) { + properties.put(CLOAK_URL_PROPERTY, new JSONArray(Collections.singleton(pp.cloak.url))); + properties.put(CLOAK_DIGEST_PROPERTY, new JSONArray(Collections.singleton( + SecurityHelper.toHex(pp.skin.digest)))); } Collections.addAll(args, "--userProperties", properties.toString()); diff --git a/Launcher/source/client/PlayerProfile.java b/Launcher/source/client/PlayerProfile.java index af44f7f..8976265 100644 --- a/Launcher/source/client/PlayerProfile.java +++ b/Launcher/source/client/PlayerProfile.java @@ -1,41 +1,43 @@ package launcher.client; import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.util.Objects; import java.util.UUID; import launcher.LauncherAPI; import launcher.helper.IOHelper; +import launcher.helper.SecurityHelper; import launcher.helper.VerifyHelper; import launcher.serialize.HInput; import launcher.serialize.HOutput; import launcher.serialize.stream.StreamObject; public final class PlayerProfile extends StreamObject { + @LauncherAPI public static final int TEXTURE_TIMEOUT = VerifyHelper.verifyInt( + Integer.parseUnsignedInt(System.getProperty("launcher.textureTimeout", Integer.toString(5000))), + VerifyHelper.POSITIVE, "launcher.textureTimeout can't be <= 0"); + // Instance @LauncherAPI public final UUID uuid; @LauncherAPI public final String username; - - // Textures properties - @LauncherAPI public final String skinURL; - @LauncherAPI public final String cloakURL; + @LauncherAPI public final Texture skin, cloak; @LauncherAPI public PlayerProfile(HInput input) throws IOException { uuid = input.readUUID(); username = VerifyHelper.verifyUsername(input.readASCII(16)); - - // Read textures - skinURL = input.readBoolean() ? IOHelper.verifyURL(input.readASCII(2048)) : null; - cloakURL = input.readBoolean() ? IOHelper.verifyURL(input.readASCII(2048)) : null; + skin = input.readBoolean() ? new Texture(input) : null; + cloak = input.readBoolean() ? new Texture(input) : null; } @LauncherAPI - public PlayerProfile(UUID uuid, String username, String skinURL, String cloakURL) { + public PlayerProfile(UUID uuid, String username, Texture skin, Texture cloak) { this.uuid = Objects.requireNonNull(uuid, "uuid"); this.username = VerifyHelper.verifyUsername(username); - this.skinURL = skinURL == null ? null : IOHelper.verifyURL(skinURL); - this.cloakURL = cloakURL == null ? null : IOHelper.verifyURL(cloakURL); + this.skin = skin; + this.cloak = cloak; } @Override @@ -44,13 +46,13 @@ output.writeASCII(username, 16); // Write textures - output.writeBoolean(skinURL != null); - if (skinURL != null) { - output.writeASCII(skinURL, 2048); + output.writeBoolean(skin != null); + if (skin != null) { + skin.write(output); } - output.writeBoolean(cloakURL != null); - if (cloakURL != null) { - output.writeASCII(cloakURL, 2048); + output.writeBoolean(cloak != null); + if (cloak != null) { + cloak.write(output); } } @@ -63,4 +65,35 @@ public static UUID offlineUUID(String username) { return UUID.nameUUIDFromBytes(IOHelper.encodeASCII("OfflinePlayer:" + username)); } + + public static final class Texture extends StreamObject { + @LauncherAPI public final String url; + @LauncherAPI public final byte[] digest; + + @LauncherAPI + public Texture(String url, byte[] digest) { + this.url = IOHelper.verifyURL(url); + this.digest = Objects.requireNonNull(digest, "digest"); + } + + @LauncherAPI + public Texture(String url) throws IOException { + this.url = IOHelper.verifyURL(url); + try (InputStream input = IOHelper.newInput(new URL(url), TEXTURE_TIMEOUT)) { + digest = SecurityHelper.digest(SecurityHelper.DigestAlgorithm.SHA256, input); + } + } + + @LauncherAPI + public Texture(HInput input) throws IOException { + this.url = IOHelper.verifyURL(input.readASCII(2048)); + this.digest = input.readByteArray(SecurityHelper.CRYPTO_MAX_LENGTH); + } + + @Override + public void write(HOutput output) throws IOException { + output.writeASCII(url, 2048); + output.writeByteArray(digest, SecurityHelper.CRYPTO_MAX_LENGTH); + } + } } diff --git a/Launcher/source/helper/IOHelper.java b/Launcher/source/helper/IOHelper.java index 13d3881..a756d3b 100644 --- a/Launcher/source/helper/IOHelper.java +++ b/Launcher/source/helper/IOHelper.java @@ -230,16 +230,23 @@ } @LauncherAPI - public static InputStream newInput(URL url) throws IOException { + public static InputStream newInput(URL url, int timeout) throws IOException { URLConnection connection = url.openConnection(); if (connection instanceof HttpURLConnection) { - connection.setReadTimeout(TIMEOUT); - connection.setConnectTimeout(TIMEOUT); + connection.setReadTimeout(timeout); + connection.setConnectTimeout(timeout); } + connection.setDoInput(true); + connection.setDoOutput(false); return connection.getInputStream(); } @LauncherAPI + public static InputStream newInput(URL url) throws IOException { + return newInput(url, TIMEOUT); + } + + @LauncherAPI public static InputStream newInput(Path file) throws IOException { return Files.newInputStream(file, READ_OPTIONS); } diff --git a/LauncherAuthlib/source/yggdrasil/YggdrasilMinecraftSessionService.java b/LauncherAuthlib/source/yggdrasil/YggdrasilMinecraftSessionService.java index c0f4366..aabd651 100644 --- a/LauncherAuthlib/source/yggdrasil/YggdrasilMinecraftSessionService.java +++ b/LauncherAuthlib/source/yggdrasil/YggdrasilMinecraftSessionService.java @@ -1,8 +1,5 @@ package com.mojang.authlib.yggdrasil; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.URL; import java.util.EnumMap; import java.util.Iterator; import java.util.Map; @@ -25,12 +22,6 @@ import launcher.request.uuid.ProfileByUUIDRequest; public final class YggdrasilMinecraftSessionService extends BaseMinecraftSessionService { - private static final boolean DISABLE_TEXTURES = Boolean.getBoolean("launcher.authlib.disableTextures"); - - // Constants - private static final String SKIN_HASH_PROPERTY = "l_skinHash"; - private static final String CLOAK_HASH_PROPERTY = "l_cloakHash"; - public YggdrasilMinecraftSessionService(AuthenticationService service) { super(service); LogHelper.debug("Patched MinecraftSessionService created"); @@ -74,7 +65,7 @@ // Add skin URL to textures map Iterator skinURL = profile.getProperties().get(ClientLauncher.SKIN_URL_PROPERTY).iterator(); - Iterator skinHash = profile.getProperties().get(SKIN_HASH_PROPERTY).iterator(); + Iterator skinHash = profile.getProperties().get(ClientLauncher.SKIN_DIGEST_PROPERTY).iterator(); if (skinURL.hasNext() && skinHash.hasNext()) { String urlValue = skinURL.next().getValue(); String hashValue = skinHash.next().getValue(); @@ -83,7 +74,7 @@ // Add cloak URL to textures map Iterator cloakURL = profile.getProperties().get(ClientLauncher.CLOAK_URL_PROPERTY).iterator(); - Iterator cloakHash = profile.getProperties().get(CLOAK_HASH_PROPERTY).iterator(); + Iterator cloakHash = profile.getProperties().get(ClientLauncher.CLOAK_DIGEST_PROPERTY).iterator(); if (cloakURL.hasNext() && cloakHash.hasNext()) { String urlValue = cloakURL.next().getValue(); String hashValue = cloakHash.next().getValue(); @@ -121,7 +112,7 @@ // Join server String username = profile.getName(); LogHelper.debug("joinServer, Username: '%s', Access token: %s, Server ID: %s", - username, accessToken, serverID); + username, accessToken, serverID); // Make joinServer request boolean success; @@ -138,25 +129,18 @@ } public static void fillTextureProperties(GameProfile profile, PlayerProfile pp) { - if (DISABLE_TEXTURES) { - return; // :( - } - - // Get property map PropertyMap properties = profile.getProperties(); - - // Hash skin - String skinHash = hash(pp.skinURL); - if (skinHash != null) { - properties.put(ClientLauncher.SKIN_URL_PROPERTY, new Property(ClientLauncher.SKIN_URL_PROPERTY, pp.skinURL, "")); - properties.put(SKIN_HASH_PROPERTY, new Property(SKIN_HASH_PROPERTY, skinHash, "")); + if (pp.skin != null) { + properties.put(ClientLauncher.SKIN_URL_PROPERTY, new Property( + ClientLauncher.SKIN_URL_PROPERTY, pp.skin.url, "")); + properties.put(ClientLauncher.SKIN_DIGEST_PROPERTY, new Property( + ClientLauncher.SKIN_DIGEST_PROPERTY, SecurityHelper.toHex(pp.skin.digest), "")); } - - // Hash cloak - String cloakHash = hash(pp.cloakURL); - if (cloakHash != null) { - properties.put(ClientLauncher.CLOAK_URL_PROPERTY, new Property(ClientLauncher.CLOAK_URL_PROPERTY, pp.cloakURL, "")); - properties.put(CLOAK_HASH_PROPERTY, new Property(CLOAK_HASH_PROPERTY, cloakHash, "")); + if (pp.cloak != null) { + properties.put(ClientLauncher.CLOAK_URL_PROPERTY, new Property( + ClientLauncher.CLOAK_URL_PROPERTY, pp.cloak.url, "")); + properties.put(ClientLauncher.CLOAK_DIGEST_PROPERTY, new Property( + ClientLauncher.CLOAK_DIGEST_PROPERTY, SecurityHelper.toHex(pp.cloak.digest), "")); } } @@ -165,20 +149,4 @@ fillTextureProperties(profile, pp); return profile; } - - private static String hash(String url) { - if (url == null) { - return null; - } - - // Try get digest of the texture - try { - return SecurityHelper.toHex(SecurityHelper.digest(SecurityHelper.DigestAlgorithm.SHA256, new URL(url))); - } catch (FileNotFoundException e) { - return null; // Just didn't has texture - } catch (IOException e) { - LogHelper.error("Can't hash texture: %s, %s", url, e.toString()); - return null; - } - } } diff --git a/compat/auth/uuid/ReversibleUUIDs.java b/compat/auth/uuid/ReversibleUUIDs.java index 137704f..cf3b900 100644 --- a/compat/auth/uuid/ReversibleUUIDs.java +++ b/compat/auth/uuid/ReversibleUUIDs.java @@ -16,4 +16,4 @@ // Decode and verify return VerifyHelper.verifyUsername(new String(bytes, 0, length, StandardCharsets.US_ASCII)); -} \ No newline at end of file +} diff --git a/compat/auth/uuid/uuidgen.sql b/compat/auth/uuid/uuidgen.sql index 8c8e726..d859873 100644 --- a/compat/auth/uuid/uuidgen.sql +++ b/compat/auth/uuid/uuidgen.sql @@ -5,4 +5,4 @@ SET NEW.uuid = UUID(); END IF; END; // -DELIMITER ; \ No newline at end of file +DELIMITER ; diff --git a/compat/auth/uuid/uuidgen_offline.sql b/compat/auth/uuid/uuidgen_offline.sql index 608635d..9de6cfd 100644 --- a/compat/auth/uuid/uuidgen_offline.sql +++ b/compat/auth/uuid/uuidgen_offline.sql @@ -8,4 +8,4 @@ SET NEW.uuid = CONCAT_WS('-', LEFT(@md5, 8), MID(@md5, 9, 4), MID(@md5, 13, 4), MID(@md5, 17, 4), RIGHT(@md5, 12)); END IF; END; // -DELIMITER ; \ No newline at end of file +DELIMITER ;