package info.mineshafter.intercept; import info.mineshafter.util.Streams; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.HttpsURLConnection; public class TextureHandler implements Handler { private Map<String, URL> skinLookup = new ConcurrentHashMap<String, URL>(); private Map<String, URL> capeLookup = new ConcurrentHashMap<String, URL>(); private static String textureHost = "textures.minecraft.net"; private static Pattern textureUrl = Pattern.compile("/([0-9a-fA-F]+)"); private static TextureHandler instance; public static synchronized TextureHandler getInstance() { if (instance == null) { instance = new TextureHandler(); } return instance; } private TextureHandler() {} public boolean canHandle(URL req) { if (!textureHost.equalsIgnoreCase(req.getHost())) { return false; } Matcher m = textureUrl.matcher(req.getPath()); if (!m.matches()) { return false; } String hash = m.group(1); char type = hash.charAt(60); if (type != '0' && type != '1') { return false; } String id = hash.substring(0, 32); String idPadding = hash.substring(32, 46); String padding = createPadding(id); return padding.equals(idPadding); // If equal, it's one of ours } public Response handle(Request req) { try { System.out.println("TextureHandler.handle: " + req.getPath()); Matcher m = textureUrl.matcher(req.getPath()); m.matches(); String hash = m.group(1); String id = hash.substring(0, 32); char type = hash.charAt(60); URL skinUrl = null; if (type == '0') { // Skin skinUrl = skinLookup.get(id); } else if (type == '1') { // Cape skinUrl = capeLookup.get(id); } //System.out.println("TextureHandler.handle type: " + type); byte[] data = new byte[0]; int responseCode = 404; if (skinUrl != null) { System.out.println("TextureHandler skinurl: " + skinUrl); String protocol = skinUrl.getProtocol(); if (protocol.equalsIgnoreCase("https")) { HttpsURLConnection conn = (HttpsURLConnection) skinUrl.openConnection(); conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"); conn.setInstanceFollowRedirects(true); responseCode = conn.getResponseCode(); data = Streams.toByteArray(conn.getInputStream()); } else if (protocol.equalsIgnoreCase("http")) { HttpURLConnection conn = (HttpURLConnection) skinUrl.openConnection(); conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"); conn.setInstanceFollowRedirects(true); responseCode = conn.getResponseCode(); data = Streams.toByteArray(conn.getInputStream()); } System.out.println("Got " + Integer.toString(data.length) + " bytes of skin data"); if (data.length == 0) { return new Response(responseCode, data); } } else { System.out.println("TextureHandler 404"); return new Response(404, data); } System.out.println("Texture response code: " + responseCode); if (data.length == 0) { return new Response(responseCode, data); } return new Response(data); } catch (IOException e) { e.printStackTrace(); return null; } } // Use the URL in the padding to make sure that when someone changes their skin, the cache doesn't keep serving the same one public String addSkin(String id, String skinUrl) { try { URL url = new URL(skinUrl); url = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile()); // Make sure we don't loop getting a handled url skinLookup.put(id, url); } catch (MalformedURLException e) { e.printStackTrace(); return skinUrl; } // id: 32 chars, idPadding: 14 chars, urlPadding: 14 chars, type: 1 char String r = "http://textures.minecraft.net/" + id + createPadding(id) + createPadding(skinUrl) + "0"; //System.out.println(r + " = " + id + ":" + skinUrl); return r; } public String addCape(String id, String capeUrl) { try { URL url = new URL(capeUrl); url = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile()); capeLookup.put(id, url); } catch (MalformedURLException e) { e.printStackTrace(); return capeUrl; } String r = "http://textures.minecraft.net/" + id + createPadding(id) + createPadding(capeUrl) + "1"; //System.out.println(r + " = " + id + ":" + capeUrl); return r; } private static String createPadding(String str) { return createPadding(str, 14); } private static String createPadding(String str, int chars) { try { // id length is 16 bytes, 32 characters MessageDigest d = MessageDigest.getInstance("SHA-1"); String hash = bytesToHex(d.digest(str.getBytes())); String padding = hash.substring(0, chars); return padding; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } private static char[] hexList = "0123456789abcdef".toCharArray(); private static String bytesToHex(byte[] buf) { StringBuilder sb = new StringBuilder(buf.length * 2); for (byte b : buf) { sb.append(hexList[(b >>> 4) & 0x0f]); sb.append(hexList[b & 0x0f]); } return sb.toString(); } }