diff --git a/build.gradle b/build.gradle index edfaf25..d0a2e3a 100644 --- a/build.gradle +++ b/build.gradle @@ -11,15 +11,17 @@ repositories { group = 'net.minecraft' archivesBaseName = 'launchwrapper' -version = '1.12' +version = '1.13' sourceCompatibility = 1.6 dependencies { compile 'net.sf.jopt-simple:jopt-simple:4.5' compile 'org.ow2.asm:asm-debug-all:5.0.3' compile 'org.lwjgl.lwjgl:lwjgl:2.9.1' - compile 'org.apache.logging.log4j:log4j-core:2.0-beta9' - compile 'org.apache.logging.log4j:log4j-api:2.0-beta9' + compile 'org.apache.logging.log4j:log4j-core:2.16.0' + compile 'org.apache.logging.log4j:log4j-api:2.16.0' + compile 'org.apache.logging.log4j:log4j-slf4j18-impl:2.16.0' + compile 'com.mojang:authlib:2.3.31' } task sourcesJar(type: Jar) { diff --git a/src/main/java/net/minecraft/launchwrapper/Launch.java b/src/main/java/net/minecraft/launchwrapper/Launch.java index 3fe49e7..e72e493 100644 --- a/src/main/java/net/minecraft/launchwrapper/Launch.java +++ b/src/main/java/net/minecraft/launchwrapper/Launch.java @@ -15,9 +15,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.net.URL; import org.apache.logging.log4j.Level; +import net.minecraft.launchwrapper.protocol.LegacyProtocolURLStreamHandlerFactory; + public class Launch { private static final String DEFAULT_TWEAK = "net.minecraft.launchwrapper.VanillaTweaker"; public static File minecraftHome; @@ -31,6 +34,7 @@ public static void main(String[] args) { public static LaunchClassLoader classLoader; private Launch() { + URL.setURLStreamHandlerFactory(new LegacyProtocolURLStreamHandlerFactory()); final URLClassLoader ucl = (URLClassLoader) getClass().getClassLoader(); classLoader = new LaunchClassLoader(ucl.getURLs()); blackboard = new HashMap(); diff --git a/src/main/java/net/minecraft/launchwrapper/protocol/CapeURLConnection.java b/src/main/java/net/minecraft/launchwrapper/protocol/CapeURLConnection.java new file mode 100644 index 0000000..0cad4ec --- /dev/null +++ b/src/main/java/net/minecraft/launchwrapper/protocol/CapeURLConnection.java @@ -0,0 +1,98 @@ +package net.minecraft.launchwrapper.protocol; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; +import java.net.Proxy; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.ProfileLookupCallback; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.Agent; +import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; + +public class CapeURLConnection extends HttpURLConnection { + + public final static String[] OLD_CAPE_ADDRESSES = new String[] { + "http://www.minecraft.net/cloak/get.jsp?user=", // Introduced Beta 1.0 (when capes were added) + "http://s3.amazonaws.com/MinecraftCloaks/", // Introduced Beta 1.2 + "http://skins.minecraft.net/MinecraftCloaks/" // Introduced Release 1.3.1 + }; + + public CapeURLConnection(URL url) { + super(url); + } + + @Override + public void disconnect() { + } + + @Override + public boolean usingProxy() { + return false; + } + + InputStream inputStream = null; + int responseCode = 200; + + private String getUsernameFromURL() { + String username = this.url.toString(); + + // We get the username from the skin by replacing the url up to the username with whitespace. + for (String oldCapeAddress : OLD_CAPE_ADDRESSES) { + username = username.replace(oldCapeAddress, ""); + } + /// ... and dropping the .png. + username = username.replace(".png", ""); + + return username; + } + + @Override + public void connect() throws IOException { + String username = getUsernameFromURL(); + + try { + MinecraftProfileTexture cape = getUserCape(username); + inputStream = new URL(cape.getUrl()).openConnection().getInputStream(); + } catch (Exception ex) { + responseCode = 404; + } + } + + YggdrasilAuthenticationService authenticationService = new YggdrasilAuthenticationService(Proxy.NO_PROXY, (String)null); + GameProfile gameProfile = null; + + private MinecraftProfileTexture getUserCape(String username) { + authenticationService.createProfileRepository().findProfilesByNames(new String[] { username }, Agent.MINECRAFT, new ProfileLookupCallback() { + public void onProfileLookupSucceeded(GameProfile paramGameProfile) { + gameProfile = paramGameProfile; + } + public void onProfileLookupFailed(GameProfile paramGameProfile, Exception paramException) { + } + }); + + if (gameProfile == null) + return null; + + gameProfile = authenticationService.createMinecraftSessionService().fillProfileProperties(gameProfile, true); + + Map textures = authenticationService.createMinecraftSessionService().getTextures(gameProfile, true); + if (textures.containsKey(MinecraftProfileTexture.Type.CAPE)) + return textures.get(MinecraftProfileTexture.Type.CAPE); + + return null; + } + + @Override + public InputStream getInputStream() throws IOException { + return inputStream; + } + + @Override + public int getResponseCode() { + return responseCode; + } +} diff --git a/src/main/java/net/minecraft/launchwrapper/protocol/JoinServerURLConnection.java b/src/main/java/net/minecraft/launchwrapper/protocol/JoinServerURLConnection.java new file mode 100644 index 0000000..05e90d3 --- /dev/null +++ b/src/main/java/net/minecraft/launchwrapper/protocol/JoinServerURLConnection.java @@ -0,0 +1,98 @@ +package net.minecraft.launchwrapper.protocol; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.Proxy; +import java.util.Map; +import java.util.HashMap; + +import com.mojang.authlib.ProfileLookupCallback; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.Agent; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; + +public class JoinServerURLConnection extends HttpURLConnection { + public JoinServerURLConnection(URL url) { + super(url); + } + + @Override + public void disconnect() { + + } + + @Override + public boolean usingProxy() { + return false; + } + + private String response = "bad login"; + + @Override + public void connect() throws IOException { + + } + + YggdrasilAuthenticationService authenticationService = new YggdrasilAuthenticationService(Proxy.NO_PROXY, (String)null); + + GameProfile gameProfile = null; + + @Override + public InputStream getInputStream() throws IOException { + // Pull params from the URL query. + String[] params = this.url.getQuery().split("&"); + Map queryMap = new HashMap(); + + for (String param : params) { + String name = param.split("=")[0]; + String value = param.split("=")[1]; + queryMap.put(name, value); + } + + String username = queryMap.get("user"); + // sessionId is token::. We want the access token. + String accessToken = queryMap.get("sessionId").split(":")[1]; + String serverId = queryMap.get("serverId"); + + // Lookup the game profile by username. + authenticationService.createProfileRepository().findProfilesByNames(new String[] { username }, Agent.MINECRAFT, new ProfileLookupCallback() { + public void onProfileLookupSucceeded(GameProfile paramGameProfile) { + gameProfile = paramGameProfile; + } + public void onProfileLookupFailed(GameProfile paramGameProfile, Exception paramException) { + } + }); + + if (gameProfile == null) + return null; + + // Send the join request. + try { + authenticationService.createMinecraftSessionService().joinServer( + gameProfile, + accessToken, + serverId + ); + + response = "ok"; + } catch (AuthenticationException ex) { + // response defaults to "bad login" + } + + return new ByteArrayInputStream(response.getBytes()); + } + + @Override + public int getResponseCode() { + return 200; + } + + @Override + public String getResponseMessage() { + return "ok"; + } +} \ No newline at end of file diff --git a/src/main/java/net/minecraft/launchwrapper/protocol/LegacyProtocolURLStreamHandler.java b/src/main/java/net/minecraft/launchwrapper/protocol/LegacyProtocolURLStreamHandler.java new file mode 100644 index 0000000..c2d3093 --- /dev/null +++ b/src/main/java/net/minecraft/launchwrapper/protocol/LegacyProtocolURLStreamHandler.java @@ -0,0 +1,42 @@ +package net.minecraft.launchwrapper.protocol; + +import java.io.IOException; +import java.net.URL; +import java.net.Proxy; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +public class LegacyProtocolURLStreamHandler extends URLStreamHandler { + private final Class defaultHttpConnectionClass; + + public LegacyProtocolURLStreamHandler(Class _defaultHttpConnectionClass) { + defaultHttpConnectionClass = _defaultHttpConnectionClass; + } + + @Override + protected URLConnection openConnection(URL url) throws IOException { + // Skins are pulled from the new endpoint and converted to the legacy format as required. + for (String oldSkinAddress : SkinURLConnection.OLD_SKIN_ADDRESSES) { + if (url.toString().startsWith(oldSkinAddress)) + return new SkinURLConnection(url); + } + + // Capes are pulled from the new endpoint, no conversion is required. + for (String oldCapeAddress : CapeURLConnection.OLD_CAPE_ADDRESSES) { + if (url.toString().startsWith(oldCapeAddress)) + return new CapeURLConnection(url); + } + + // Server authentication is done over a newer endpoint. + if (url.toString().startsWith("http://www.minecraft.net/game/joinserver.jsp")) { + return new JoinServerURLConnection(url); + } + + try { + return defaultHttpConnectionClass.getConstructor(URL.class, Proxy.class).newInstance(url, Proxy.NO_PROXY); + } catch (Exception e) { + // If the constructor isn't found, you can log that out. It's not expected. + return null; + } + } +} diff --git a/src/main/java/net/minecraft/launchwrapper/protocol/LegacyProtocolURLStreamHandlerFactory.java b/src/main/java/net/minecraft/launchwrapper/protocol/LegacyProtocolURLStreamHandlerFactory.java new file mode 100644 index 0000000..7f97b32 --- /dev/null +++ b/src/main/java/net/minecraft/launchwrapper/protocol/LegacyProtocolURLStreamHandlerFactory.java @@ -0,0 +1,30 @@ +package net.minecraft.launchwrapper.protocol; + +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +public class LegacyProtocolURLStreamHandlerFactory implements URLStreamHandlerFactory { + private final Class defaultHttpConnectionClass; + + public LegacyProtocolURLStreamHandlerFactory() { + try { + URL foo = new URL("http://example.com"); + // Doesn't actually establish a connection + defaultHttpConnectionClass = foo.openConnection().getClass(); + } catch (Exception e) { + // this should never happen as the URL is hardcoded, shouldn't be invalid. + throw new RuntimeException(e); + } + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if ("http".equals(protocol)) { + return new LegacyProtocolURLStreamHandler(defaultHttpConnectionClass); + } + + return null; + } +} diff --git a/src/main/java/net/minecraft/launchwrapper/protocol/SkinURLConnection.java b/src/main/java/net/minecraft/launchwrapper/protocol/SkinURLConnection.java new file mode 100644 index 0000000..58d133f --- /dev/null +++ b/src/main/java/net/minecraft/launchwrapper/protocol/SkinURLConnection.java @@ -0,0 +1,144 @@ +package net.minecraft.launchwrapper.protocol; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.URL; +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.awt.image.BufferedImage; +import java.awt.*; +import java.util.Map; +import java.net.Proxy; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.ProfileLookupCallback; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.Agent; +import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; + +public class SkinURLConnection extends HttpURLConnection { + + public final static String[] OLD_SKIN_ADDRESSES = new String[] { + "http://www.minecraft.net/skin/", // Introduced Classic 0.0.18a (when skins were added) + "http://s3.amazonaws.com/MinecraftSkins/", // Introduced Beta 1.2 + "http://skins.minecraft.net/MinecraftSkins/" // Introduced Release 1.3.1 + }; + + public SkinURLConnection(URL url) { + super(url); + } + + @Override + public void disconnect() { + } + + @Override + public boolean usingProxy() { + return false; + } + + InputStream inputStream = null; + int responseCode = 200; + + private String getUsernameFromURL() { + String username = this.url.toString(); + + // We get the username from the skin by replacing the url up to the username with whitespace. + for (String oldSkinAddress : OLD_SKIN_ADDRESSES) { + username = username.replace(oldSkinAddress, ""); + } + /// ... and dropping the .png. + username = username.replace(".png", ""); + + return username; + } + + @Override + public void connect() throws IOException { + String username = getUsernameFromURL(); + + try { + MinecraftProfileTexture skin = getUserSkin(username); + boolean slim = "slim".equals(skin.getMetadata("model")); + inputStream = convertModernSkin(new URL(skin.getUrl()), slim); + } catch (Exception ex) { + responseCode = 404; + } + } + + GameProfile gameProfile = null; + YggdrasilAuthenticationService authenticationService = new YggdrasilAuthenticationService(Proxy.NO_PROXY, (String)null); + + private MinecraftProfileTexture getUserSkin(String username) { + authenticationService.createProfileRepository().findProfilesByNames(new String[] { username }, Agent.MINECRAFT, new ProfileLookupCallback() { + public void onProfileLookupSucceeded(GameProfile paramGameProfile) { + gameProfile = paramGameProfile; + } + public void onProfileLookupFailed(GameProfile paramGameProfile, Exception paramException) { + } + }); + + if (gameProfile == null) + return null; + + gameProfile = authenticationService.createMinecraftSessionService().fillProfileProperties(gameProfile, true); + + Map textures = authenticationService.createMinecraftSessionService().getTextures(gameProfile, true); + + if (textures.containsKey(MinecraftProfileTexture.Type.SKIN)) + return textures.get(MinecraftProfileTexture.Type.SKIN); + + return null; + } + + public static InputStream convertModernSkin(URL skinUrl, boolean slim) throws IOException { + InputStream inputStream = skinUrl.openStream(); + BufferedImage skin = ImageIO.read(inputStream); + boolean tall = skin.getHeight() > 32; + BufferedImage movePart = null; + Graphics2D graphics = skin.createGraphics(); + AlphaComposite alpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER); + graphics.setComposite(alpha); + + if (tall) { + // Flatten second layers. + movePart = skin.getSubimage(0, 32, 56, 16); + graphics.drawImage(movePart, 0, 16, null); + } + + if (slim) { + // Convert alex to steve. + + // Stretch right arm. + movePart = skin.getSubimage(45, 16, 9, 16); + graphics.drawImage(movePart, 46, 16, null); + movePart = skin.getSubimage(49, 16, 2, 4); + graphics.drawImage(movePart, 50, 16, null); + movePart = skin.getSubimage(53, 20, 2, 12); + graphics.drawImage(movePart, 54, 20, null); + } + + graphics.dispose(); + + // Crop + BufferedImage croppedSkin = skin.getSubimage(0, 0, 64, 32); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ImageIO.write(croppedSkin, "png", os); + byte[] bytes = os.toByteArray(); + return new ByteArrayInputStream(bytes); + } + + @Override + public InputStream getInputStream() throws IOException { + return inputStream; + } + + @Override + public int getResponseCode() { + return responseCode; + } +}