/*
* This file is part of Sponge, licensed under the MIT License (MIT).
*
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.spongepowered.server.launch;
import static org.spongepowered.server.launch.VanillaCommandLine.HELP;
import static org.spongepowered.server.launch.VanillaCommandLine.NO_DOWNLOAD;
import static org.spongepowered.server.launch.VanillaCommandLine.NO_VERIFY_CLASSPATH;
import static org.spongepowered.server.launch.VanillaCommandLine.TWEAK_CLASS;
import static org.spongepowered.server.launch.VanillaCommandLine.VERSION;
import joptsimple.BuiltinHelpFormatter;
import joptsimple.OptionSet;
import net.minecraft.launchwrapper.Launch;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
public final class VanillaServerMain {
private static final String LIBRARIES_DIR = "libraries";
private static final String MINECRAFT_SERVER_LOCAL = "minecraft_server.1.11.2.jar";
private static final String MINECRAFT_SERVER_REMOTE = "https://s3.amazonaws.com/Minecraft.Download/versions/1.11.2/minecraft_server.1.11.2.jar";
private static final String LAUNCHWRAPPER_PATH = "/net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar";
private static final String LAUNCHWRAPPER_LOCAL = LIBRARIES_DIR + LAUNCHWRAPPER_PATH;
private static final String LAUNCHWRAPPER_REMOTE = "https://libraries.minecraft.net" + LAUNCHWRAPPER_PATH;
private static final String TWEAK_ARGUMENT = "--tweakClass";
private static final String TWEAKER = "org.spongepowered.server.launch.VanillaServerTweaker";
private VanillaServerMain() {
}
public static void main(String[] args) throws Exception {
OptionSet options = VanillaCommandLine.parse(args);
if (options.has(HELP)) {
if (System.console() == null) {
// We have no supported terminal, print help with default terminal width
VanillaCommandLine.printHelp(System.err);
} else {
// Terminal is (very likely) supported, use the terminal width provided by jline
Terminal terminal = TerminalBuilder.builder().dumb(true).build();
VanillaCommandLine.printHelp(new BuiltinHelpFormatter(terminal.getWidth(), 3), System.err);
}
return;
} else if (options.has(VERSION)) {
final Package pack = VanillaServerMain.class.getPackage();
System.out.println(pack.getImplementationTitle() + ' ' + pack.getImplementationVersion());
System.out.println(pack.getSpecificationTitle() + ' ' + pack.getSpecificationVersion());
return;
}
// Download/verify Minecraft server installation if necessary and not disabled
if (!options.has(NO_VERIFY_CLASSPATH)) {
// Get the location of our jar
Path base = Paths.get(VanillaServerMain.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent();
try {
// Download dependencies
if (!downloadMinecraft(base, !options.has(NO_DOWNLOAD))) {
System.err.println("Failed to load all required dependencies. Please download them manually:");
System.err.println("Download " + MINECRAFT_SERVER_REMOTE + " and copy it to "
+ base.resolve(MINECRAFT_SERVER_LOCAL).toAbsolutePath());
System.err.println("Download " + LAUNCHWRAPPER_REMOTE + " and copy it to "
+ base.resolve(LAUNCHWRAPPER_LOCAL).toAbsolutePath());
System.exit(1);
return;
}
} catch (IOException e) {
System.err.println("Failed to download required dependencies. Please try again later.");
e.printStackTrace();
System.exit(1);
return;
}
} else {
System.err.println("Classpath verification is disabled. The server may NOT start properly unless you have all required dependencies on "
+ "the classpath!");
}
Launch.main(getLaunchArguments(TWEAKER, options.valuesOf(TWEAK_CLASS)));
}
private static String[] getLaunchArguments(String primaryTweaker, List<String> tweakers) {
if (tweakers.isEmpty()) {
return new String[]{TWEAK_ARGUMENT, primaryTweaker};
}
String[] result = new String[tweakers.size() * 2 + 2];
result[0] = TWEAK_ARGUMENT;
result[1] = primaryTweaker;
int i = 2;
for (String tweaker : tweakers) {
result[i++] = TWEAK_ARGUMENT;
result[i++] = tweaker;
}
return result;
}
private static boolean downloadMinecraft(Path base, boolean autoDownload) throws IOException, NoSuchAlgorithmException {
// Make sure the Minecraft server is available, or download it otherwise
Path path = base.resolve(MINECRAFT_SERVER_LOCAL);
if (Files.notExists(path) && (!autoDownload || !downloadVerified(MINECRAFT_SERVER_REMOTE, path))) {
return false;
}
// Make sure Launchwrapper is available, or download it otherwise
path = base.resolve(LAUNCHWRAPPER_LOCAL);
return Files.exists(path) || (autoDownload && downloadVerified(LAUNCHWRAPPER_REMOTE, path));
}
private static boolean downloadVerified(String remote, Path path) throws IOException, NoSuchAlgorithmException {
Files.createDirectories(path.getParent());
String name = path.getFileName().toString();
URL url = new URL(remote);
System.out.println("Downloading " + name + "... This can take a while.");
System.out.println(url);
URLConnection con = url.openConnection();
MessageDigest md5 = MessageDigest.getInstance("MD5");
try (ReadableByteChannel source = Channels.newChannel(new DigestInputStream(con.getInputStream(), md5));
FileChannel out = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
out.transferFrom(source, 0, Long.MAX_VALUE);
}
String expected = getETag(con);
if (!expected.isEmpty()) {
String hash = toHexString(md5.digest());
if (hash.equals(expected)) {
System.out.println("Successfully downloaded " + name + " and verified checksum!");
} else {
Files.delete(path);
throw new IOException("Checksum verification failed: Expected " + expected + ", got " + hash);
}
}
return true;
}
private static String getETag(URLConnection con) {
String hash = con.getHeaderField("ETag");
if (hash == null || hash.isEmpty()) {
return "";
}
if (hash.startsWith("\"") && hash.endsWith("\"")) {
hash = hash.substring(1, hash.length() - 1);
}
return hash;
}
// From http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
private static final char[] hexArray = "0123456789abcdef".toCharArray();
public static String toHexString(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}