package net.minecraft; import java.applet.Applet; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilePermission; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.SocketPermission; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.security.AccessControlException; import java.security.AccessController; import java.security.CodeSource; import java.security.PermissionCollection; import java.security.PrivilegedExceptionAction; import java.security.SecureClassLoader; import java.util.Enumeration; import java.util.Vector; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; public class GameUpdater implements Runnable { public static final int STATE_INIT = 1; public static final int STATE_DETERMINING_PACKAGES = 2; public static final int STATE_CHECKING_CACHE = 3; public static final int STATE_DOWNLOADING = 4; public static final int STATE_EXTRACTING_PACKAGES = 5; public static final int STATE_UPDATING_CLASSPATH = 6; public static final int STATE_SWITCHING_APPLET = 7; public static final int STATE_INITIALIZE_REAL_APPLET = 8; public static final int STATE_START_REAL_APPLET = 9; public static final int STATE_DONE = 10; public int percentage; public int currentSizeDownload; public int totalSizeDownload; public int currentSizeExtract; public int totalSizeExtract; protected URL librariesUrl; protected URL nativesUrl; private static ClassLoader classLoader; protected Thread loaderThread; protected Thread animationThread; public boolean fatalError; public String fatalErrorDescription; protected String subtaskMessage = ""; protected int state = 1; protected String[] genericErrorMessage = new String[]{"An error occured while loading the applet.", "Please contact support to resolve this issue.", ""}; protected boolean certificateRefused; protected String[] certificateRefusedMessage = new String[]{"Permissions for Applet Refused.", "Please accept the permissions dialog to allow", "the applet to continue the loading process."}; protected static boolean natives_loaded = false; private final GameVersion latestVersion; public GameUpdater(GameVersion latestVersion) { this.latestVersion = latestVersion; } public void init() { this.state = STATE_INIT; if (!latestVersion.hasFullInfo()) { try { latestVersion.downloadInfo(); } catch (IOException e) { fatalErrorOccured("Unable to get info for version", e); } } } private String generateStacktrace(Exception exception) { Writer result = new StringWriter(); PrintWriter printWriter = new PrintWriter(result); exception.printStackTrace(printWriter); return result.toString(); } protected String getDescriptionForState() { switch (this.state) { case STATE_INIT: return "Initializing loader"; case STATE_DETERMINING_PACKAGES: return "Determining packages to load"; case STATE_CHECKING_CACHE: return "Checking cache for existing files"; case STATE_DOWNLOADING: return "Downloading packages"; case STATE_EXTRACTING_PACKAGES: return "Extracting downloaded packages"; case STATE_UPDATING_CLASSPATH: return "Updating classpath"; case STATE_SWITCHING_APPLET: return "Switching applet"; case STATE_INITIALIZE_REAL_APPLET: return "Initializing real applet"; case STATE_START_REAL_APPLET: return "Starting real applet"; case STATE_DONE: return "Done loading"; } return "unknown state"; } protected void loadJarURLs() throws Exception { this.state = STATE_DETERMINING_PACKAGES; URL path = new URL("http://files.betacraft.uk/launcher/assets/"); String osName = System.getProperty("os.name"); String librariesZip; String nativesZip; if (osName.startsWith("Win")) { librariesZip = "libs-windows.zip"; nativesZip = "natives-windows.zip"; } else if (osName.startsWith("Linux")) { librariesZip = "libs-linux.zip"; nativesZip = "natives-linux.zip"; } else if (osName.startsWith("Mac")) { librariesZip = "libs-osx.zip"; nativesZip = "natives-osx.zip"; } else { fatalErrorOccured("OS (" + osName + ") not supported", null); return; } nativesUrl = new URL(path, nativesZip); librariesUrl = new URL(path, librariesZip); } public void run() { init(); this.state = STATE_CHECKING_CACHE; this.percentage = 5; try { loadJarURLs(); String path = AccessController.doPrivileged((PrivilegedExceptionAction) () -> Util.getWorkingDirectory() + File.separator + "bin" + File.separator); File dir = new File(path); if (!dir.exists()) { dir.mkdirs(); } if (this.latestVersion != null) { File versionFile = new File(dir, "version"); boolean cacheAvailable = false; if (versionFile.exists() && (this.latestVersion.equals("-1") || latestVersion.isSameVersion(versionFile))) { cacheAvailable = true; this.percentage = 90; } if (!cacheAvailable) { downloadJars(path); extractJars(path); extractNatives(path); this.percentage = 90; latestVersion.writeVersionFile(versionFile); } } updateClassPath(dir); this.state = STATE_DONE; } catch (AccessControlException ace) { fatalErrorOccured(ace.getMessage(), ace); this.certificateRefused = true; } catch (Exception e) { fatalErrorOccured(e.getMessage(), e); } finally { this.loaderThread = null; } } protected String readVersionFile(File file) throws Exception { DataInputStream dis = new DataInputStream(new FileInputStream(file)); String version = dis.readUTF(); dis.close(); return version; } protected void updateClassPath(File dir) throws Exception { this.state = STATE_UPDATING_CLASSPATH; this.percentage = 95; File[] files = dir.listFiles((dir1, name) -> name.endsWith(".jar")); URL[] urls = new URL[files.length]; for (int i = 0; i < files.length; i++) { urls[i] = files[i].toURI().toURL(); } if (classLoader == null) { classLoader = new URLClassLoader(urls) { protected PermissionCollection getPermissions(CodeSource codesource) { PermissionCollection perms = null; try { Method method = SecureClassLoader.class.getDeclaredMethod("getPermissions", CodeSource.class); method.setAccessible(true); perms = (PermissionCollection) method.invoke(getClass().getClassLoader(), new Object[]{codesource}); String host = "www.minecraft.net"; if (host != null && host.length() > 0) { perms.add(new SocketPermission(host, "connect,accept")); } else { codesource.getLocation().getProtocol().equals("file"); } perms.add(new FilePermission("<>", "read")); } catch (Exception e) { e.printStackTrace(); } return perms; } }; } String path = dir.getAbsolutePath(); if (!path.endsWith(File.separator)) path = path + File.separator; unloadNatives(path); System.setProperty("org.lwjgl.librarypath", path + "natives"); System.setProperty("net.java.games.input.librarypath", path + "natives"); natives_loaded = true; } private void unloadNatives(String nativePath) { if (!natives_loaded) { return; } try { Field field = ClassLoader.class.getDeclaredField("loadedLibraryNames"); field.setAccessible(true); Vector libs = (Vector) field.get(getClass().getClassLoader()); String path = (new File(nativePath)).getCanonicalPath(); for (int i = 0; i < libs.size(); i++) { String s = libs.get(i); if (s.startsWith(path)) { libs.remove(i); i--; } } } catch (Exception e) { e.printStackTrace(); } } public Applet createApplet() throws ClassNotFoundException, InstantiationException, IllegalAccessException { Class appletClass = (Class) classLoader.loadClass("net.minecraft.client.MinecraftApplet"); return appletClass.newInstance(); } protected int getRemoteFileSize(URL remoteFileUrl) throws IOException { System.out.println(remoteFileUrl); URLConnection urlconnection = remoteFileUrl.openConnection(); urlconnection.setDefaultUseCaches(false); if (urlconnection instanceof HttpURLConnection) { ((HttpURLConnection) urlconnection).setRequestMethod("HEAD"); } return urlconnection.getContentLength(); } protected void downloadRemoteFile(URL remoteFileUrl, String path, String fileName, int expectedSize, int initialPercentage) throws Exception { byte[] buffer = new byte[65536]; int unsuccessfulAttempts = 0; int maxUnsuccessfulAttempts = 3; boolean downloadFile = true; while (downloadFile) { downloadFile = false; URLConnection urlconnection = remoteFileUrl.openConnection(); if (urlconnection instanceof HttpURLConnection) { urlconnection.setRequestProperty("Cache-Control", "no-cache"); urlconnection.connect(); } String currentFile = getFileName(remoteFileUrl); InputStream inputstream = getJarInputStream(currentFile, urlconnection); FileOutputStream fos = new FileOutputStream(path + fileName); long downloadStartTime = System.currentTimeMillis(); int downloadedAmount = 0; int fileSize = 0; String downloadSpeedMessage = ""; int bufferSize; while ((bufferSize = inputstream.read(buffer, 0, buffer.length)) != -1) { fos.write(buffer, 0, bufferSize); this.currentSizeDownload += bufferSize; fileSize += bufferSize; this.percentage = initialPercentage + this.currentSizeDownload * 45 / this.totalSizeDownload; this.subtaskMessage = "Retrieving: " + currentFile + " " + (this.currentSizeDownload * 100 / this.totalSizeDownload) + "%"; downloadedAmount += bufferSize; long timeLapse = System.currentTimeMillis() - downloadStartTime; if (timeLapse >= 1000L) { float downloadSpeed = downloadedAmount / (float) timeLapse; downloadSpeed = (int) (downloadSpeed * 100.0F) / 100.0F; downloadSpeedMessage = " @ " + downloadSpeed + " KB/sec"; downloadedAmount = 0; downloadStartTime += 1000L; } this.subtaskMessage = this.subtaskMessage + downloadSpeedMessage; } inputstream.close(); fos.close(); if (urlconnection instanceof HttpURLConnection && fileSize != expectedSize) { if (expectedSize > 0) { unsuccessfulAttempts++; if (unsuccessfulAttempts < maxUnsuccessfulAttempts) { downloadFile = true; this.currentSizeDownload -= fileSize; continue; } throw new Exception("failed to download " + currentFile); } } } } protected void downloadJars(String path) throws Exception { this.state = STATE_DOWNLOADING; int gameJarSize = getRemoteFileSize(latestVersion.gameJarUrl); int librariesSize = getRemoteFileSize(librariesUrl); int nativesSize = getRemoteFileSize(nativesUrl); totalSizeDownload += gameJarSize + librariesSize + nativesSize; int initialPercentage = this.percentage = 10; downloadRemoteFile(latestVersion.gameJarUrl, path, "minecraft.jar", gameJarSize, initialPercentage); downloadRemoteFile(librariesUrl, path, getFileName(librariesUrl), librariesSize, initialPercentage); downloadRemoteFile(nativesUrl, path, getFileName(nativesUrl), nativesSize, initialPercentage); this.subtaskMessage = ""; } protected InputStream getJarInputStream(String currentFile, final URLConnection urlconnection) throws Exception { final InputStream[] is = new InputStream[1]; for (int j = 0; j < 3 && is[0] == null; j++) { Thread t = new Thread() { public void run() { try { is[0] = urlconnection.getInputStream(); } catch (IOException iOException) { } } }; t.setName("JarInputStreamThread"); t.start(); int iterationCount = 0; while (is[0] == null && iterationCount++ < 5) { try { t.join(1000L); } catch (InterruptedException interruptedException) { } } if (is[0] == null) { try { t.interrupt(); t.join(); } catch (InterruptedException interruptedException) { } } } if (is[0] == null) { if (currentFile.equals("minecraft.jar")) { throw new Exception("Unable to download " + currentFile); } throw new Exception("Unable to download " + currentFile); } return is[0]; } protected void extractZIP(String in) throws Exception { File originalFile = new File(in); Path targetDir = originalFile.toPath().getParent(); ZipInputStream zipStream = new ZipInputStream(Files.newInputStream(originalFile.toPath())); ZipEntry entry; while ((entry = zipStream.getNextEntry()) != null) { Path resolvedPath = targetDir.resolve(entry.getName()).normalize(); if (!resolvedPath.startsWith(targetDir)) { throw new RuntimeException("Entry with illegal path: " + entry.getName()); } if (entry.isDirectory()) { Files.createDirectories(resolvedPath); } else { Files.createDirectories(resolvedPath.getParent()); Files.copy(zipStream, resolvedPath, StandardCopyOption.REPLACE_EXISTING); } } originalFile.delete(); } protected void extractJars(String path) throws Exception { this.state = STATE_EXTRACTING_PACKAGES; this.percentage = 65; // We only need to extract the library jars from the zip archive String filename = getFileName(librariesUrl); this.subtaskMessage = "Extracting: " + filename; extractZIP(path + filename); } protected void extractNatives(String path) throws Exception { this.state = STATE_EXTRACTING_PACKAGES; int initialPercentage = this.percentage; String nativeJar = getJarName(nativesUrl); /* TODO: There should still be some kind of validation probably Certificate[] certificate = Launcher.class.getProtectionDomain().getCodeSource().getCertificates(); if (certificate == null) { URL location = Launcher.class.getProtectionDomain().getCodeSource().getLocation(); JarURLConnection jurl = (JarURLConnection) (new URL("jar:" + location.toString() + "!/net/minecraft/Launcher.class")).openConnection(); jurl.setDefaultUseCaches(true); try { certificate = jurl.getCertificates(); } catch (Exception exception) { } } */ File nativeFolder = new File(path + "natives"); if (!nativeFolder.exists()) { nativeFolder.mkdir(); } ZipFile zipFile = new ZipFile(path + nativeJar); Enumeration entities = zipFile.entries(); this.totalSizeExtract = 0; while (entities.hasMoreElements()) { ZipEntry entry = entities.nextElement(); if (entry.isDirectory() || entry.getName().indexOf('/') != -1) continue; this.totalSizeExtract = (int) (this.totalSizeExtract + entry.getSize()); } this.currentSizeExtract = 0; entities = zipFile.entries(); while (entities.hasMoreElements()) { ZipEntry entry = entities.nextElement(); if (entry.isDirectory() || entry.getName().indexOf('/') != -1) continue; File file = new File(path + "natives" + File.separator + entry.getName()); if (file.exists() && !file.delete()) continue; InputStream in = zipFile.getInputStream(zipFile.getEntry(entry.getName())); OutputStream out = Files.newOutputStream(Paths.get(nativeFolder.getPath(), entry.getName())); byte[] buffer = new byte[65536]; int bufferSize; while ((bufferSize = in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, bufferSize); this.currentSizeExtract += bufferSize; this.percentage = initialPercentage + this.currentSizeExtract * 20 / this.totalSizeExtract; this.subtaskMessage = "Extracting: " + entry.getName() + " " + (this.currentSizeExtract * 100 / this.totalSizeExtract) + "%"; } in.close(); out.close(); } this.subtaskMessage = ""; zipFile.close(); File f = new File(path + nativeJar); f.delete(); } protected String getJarName(URL url) { String fileName = url.getFile(); if (fileName.contains("?")) { fileName = fileName.substring(0, fileName.indexOf("?")); } if (fileName.endsWith(".pack.lzma")) { fileName = fileName.replaceAll(".pack.lzma", ""); } else if (fileName.endsWith(".pack")) { fileName = fileName.replaceAll(".pack", ""); } else if (fileName.endsWith(".lzma")) { fileName = fileName.replaceAll(".lzma", ""); } return fileName.substring(fileName.lastIndexOf('/') + 1); } protected String getFileName(URL url) { String fileName = url.getFile(); if (fileName.contains("?")) { fileName = fileName.substring(0, fileName.indexOf("?")); } return fileName.substring(fileName.lastIndexOf('/') + 1); } protected void fatalErrorOccured(String error, Exception e) { e.printStackTrace(); this.fatalError = true; this.fatalErrorDescription = "Fatal error occured (" + this.state + "): " + error; System.out.println(this.fatalErrorDescription); if (e != null) { System.out.println(generateStacktrace(e)); } } public boolean canPlayOffline() { try { String path = AccessController.doPrivileged(new PrivilegedExceptionAction() { public String run() throws Exception { return Util.getWorkingDirectory() + File.separator + "bin" + File.separator; } }); File dir = new File(path); if (!dir.exists()) return false; dir = new File(dir, "version"); if (!dir.exists()) return false; if (dir.exists()) { String version = readVersionFile(dir); if (version != null && !version.isEmpty()) { return true; } } } catch (Exception e) { e.printStackTrace(); return false; } return false; } }