From feea22f251cd188d91b843ff9348e057e0f9e91d Mon Sep 17 00:00:00 2001 From: Cow Date: Sat, 22 Oct 2022 14:06:49 +0200 Subject: Added Let's Encrypt support for ancient Java versions --- .../java/de/cowtipper/cowlection/Cowlection.java | 2 +- .../cowlection/config/CredentialStorage.java | 96 ++++++++++++++++++++- .../de/cowtipper/cowlection/util/ApiUtils.java | 66 +++++++++----- src/main/resources/https-for-ancient-java.jks | Bin 0 -> 2040 bytes 4 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 src/main/resources/https-for-ancient-java.jks (limited to 'src') diff --git a/src/main/java/de/cowtipper/cowlection/Cowlection.java b/src/main/java/de/cowtipper/cowlection/Cowlection.java index 332d08e..2ee7e34 100644 --- a/src/main/java/de/cowtipper/cowlection/Cowlection.java +++ b/src/main/java/de/cowtipper/cowlection/Cowlection.java @@ -65,8 +65,8 @@ public class Cowlection { configDir.mkdirs(); } - friendsHandler = new FriendsHandler(this, new File(configDir, "friends.json")); moo = new CredentialStorage(new Configuration(new File(configDir, "do-not-share-me-with-other-players.cfg"))); + friendsHandler = new FriendsHandler(this, new File(configDir, "friends.json")); partyFinderRules = new Rules(this, new File(configDir, "partyfinder-rules.json")); config = new MooConfig(this, new Configuration(new File(configDir, MODID + ".cfg"), "2")); } diff --git a/src/main/java/de/cowtipper/cowlection/config/CredentialStorage.java b/src/main/java/de/cowtipper/cowlection/config/CredentialStorage.java index f4e64f6..86deda9 100644 --- a/src/main/java/de/cowtipper/cowlection/config/CredentialStorage.java +++ b/src/main/java/de/cowtipper/cowlection/config/CredentialStorage.java @@ -4,8 +4,23 @@ import de.cowtipper.cowlection.Cowlection; import de.cowtipper.cowlection.util.ApiUtils; import de.cowtipper.cowlection.util.Utils; import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.MathHelper; import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Property; +import org.apache.http.conn.ssl.SSLContexts; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Enumeration; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Key and secret holder in its own file to avoid people leaking their keys accidentally. @@ -13,6 +28,7 @@ import net.minecraftforge.common.config.Property; public class CredentialStorage { public static String moo; public static boolean isMooValid; + public static SSLContext sslContext; private Property propMoo; private Property propIsMooValid; private final Configuration cfg; @@ -20,17 +36,18 @@ public class CredentialStorage { public CredentialStorage(Configuration configuration) { cfg = configuration; initConfig(); + verifyNewerHttpsSupport(); } private void initConfig() { cfg.load(); propMoo = cfg.get(Configuration.CATEGORY_CLIENT, - "moo", "", "Don't share this with anybody! Do not edit this entry manually either!", Utils.VALID_UUID_PATTERN) + "moo", "", "Don't share this with anybody! Do not edit this entry manually either!", Utils.VALID_UUID_PATTERN) .setShowInGui(false); propMoo.setLanguageKey(Cowlection.MODID + ".config." + propMoo.getName()); propIsMooValid = cfg.get(Configuration.CATEGORY_CLIENT, - "isMooValid", false, "Is the value valid?") + "isMooValid", false, "Is the value valid?") .setShowInGui(false); moo = propMoo.getString(); isMooValid = propIsMooValid.getBoolean(); @@ -39,6 +56,81 @@ public class CredentialStorage { } } + private void verifyNewerHttpsSupport() { + String javaVersion = System.getProperty("java.version", "unknown java version"); + Pattern javaVersionPattern = Pattern.compile("1\\.8\\.0_(\\d+)"); // e.g. 1.8.0_51 + Matcher javaVersionMatcher = javaVersionPattern.matcher(javaVersion); + if (!javaVersionMatcher.matches() + || MathHelper.parseIntWithDefault(javaVersionMatcher.group(1), 1337) >= 101) { + // newer Java version (>=8u101): *should* already have support by default + return; + } + + // running Java <8u101 + if (testNewHttps()) { + // uhm... looks like someone added the certs to the default JKS already + return; + } + System.out.println("Injecting Let's Encrypt support due to ancient Java version..."); + try { + KeyStore originalKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + originalKeyStore.load(Files.newInputStream(Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts")), "changeit".toCharArray()); + + KeyStore mooKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + mooKeyStore.load(getClass().getResourceAsStream("/https-for-ancient-java.jks"), "mooveit".toCharArray()); + + KeyStore tempKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + tempKeyStore.load(null, "moovedit".toCharArray()); + + for (Enumeration aliases = originalKeyStore.aliases(); aliases.hasMoreElements(); ) { + String alias = aliases.nextElement(); + tempKeyStore.setCertificateEntry(alias, originalKeyStore.getCertificate(alias)); + } + for (Enumeration aliases = mooKeyStore.aliases(); aliases.hasMoreElements(); ) { + String alias = aliases.nextElement(); + tempKeyStore.setCertificateEntry(alias, mooKeyStore.getCertificate(alias)); + } + sslContext = SSLContexts.custom() + .loadKeyMaterial(tempKeyStore, "moovedit".toCharArray()) + .loadTrustMaterial(tempKeyStore, null) + .build(); + + if (!testNewHttps()) { + System.err.println("Error while trying to add Let's Encrypt support: Could not contact site after running setup"); + } + } catch (GeneralSecurityException | IOException e) { + System.err.println("Error while trying to add Let's Encrypt support:"); + e.printStackTrace(); + } + } + + /** + * Tests accessing a site that uses Let's Encrypt + * + * @return true, if connection was successful + */ + private boolean testNewHttps() { + try { + URLConnection connection = new URL("https://helloworld.letsencrypt.org/").openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(8000); + connection.setDoInput(false); + connection.setDoOutput(false); + connection.addRequestProperty("User-Agent", "Forge Mod " + Cowlection.MODNAME + "/" + Cowlection.VERSION + " (" + Cowlection.GITURL + ")"); + if (CredentialStorage.sslContext != null && connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(CredentialStorage.sslContext.getSocketFactory()); + } + + connection.connect(); + + // seems to be working since there was no IOException! + return true; + } catch (IOException ignored) { + // most likely a newer https related issue, so setup might fix things + } + return false; + } + public void setMooIfValid(String moo, boolean commandTriggered) { ApiUtils.fetchApiKeyInfo(moo, hyApiKey -> { if (hyApiKey != null && hyApiKey.isSuccess()) { diff --git a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java index 663936f..1397a36 100644 --- a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java +++ b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java @@ -14,9 +14,14 @@ import de.cowtipper.cowlection.data.*; import de.cowtipper.cowlection.error.ApiAskPolitelyErrorEvent; import de.cowtipper.cowlection.error.ApiHttpErrorEvent; import de.cowtipper.cowlection.error.ApiHttpErrorException; +import net.minecraft.util.EnumChatFormatting; import net.minecraftforge.common.MinecraftForge; import org.apache.http.HttpStatus; +import sun.security.provider.certpath.SunCertPathBuilderException; +import sun.security.validator.ValidatorException; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLHandshakeException; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -194,31 +199,46 @@ public class ApiUtils { } private static BufferedReader makeApiCall(String url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); - connection.setConnectTimeout(5000); - connection.setReadTimeout(8000); - connection.addRequestProperty("User-Agent", "Forge Mod " + Cowlection.MODNAME + "/" + Cowlection.VERSION + " (" + Cowlection.GITURL + ")"); - - connection.getResponseCode(); - if (connection.getResponseCode() == HttpStatus.SC_NO_CONTENT) { // http status 204 - return null; - } else if (connection.getResponseCode() == HttpStatus.SC_BAD_GATEWAY && url.startsWith("https://api.hypixel.net/")) { // http status 502 (cloudflare) - throw new ApiHttpErrorException("Couldn't contact Hypixel API (502 Bad Gateway). API might be down, check https://status.hypixel.net for info.", "https://status.hypixel.net"); - } else if (connection.getResponseCode() == HttpStatus.SC_SERVICE_UNAVAILABLE) { // http status 503 Service Unavailable - int queryParamStart = url.indexOf('?', 10); - String baseUrl = queryParamStart > 0 ? url.substring(0, queryParamStart) : url; - throw new ApiHttpErrorException("Couldn't contact the API (503 Service unavailable). API might be down, or you might be blocked by Cloudflare, check if you can reach: " + baseUrl, url); - } else if (connection.getResponseCode() == HttpStatus.SC_BAD_GATEWAY && url.startsWith("https://moulberry.codes/")) { // http status 502 (cloudflare) - throw new ApiHttpErrorException("Couldn't contact Moulberry's API (502 Bad Gateway). API might be down, check if " + LOWEST_BINS + " is reachable.", LOWEST_BINS); - } else { - BufferedReader reader; - InputStream errorStream = connection.getErrorStream(); - if (errorStream != null) { - reader = new BufferedReader(new InputStreamReader(errorStream)); + try { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + if (CredentialStorage.sslContext != null && connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(CredentialStorage.sslContext.getSocketFactory()); + } + connection.setConnectTimeout(5000); + connection.setReadTimeout(8000); + connection.addRequestProperty("User-Agent", "Forge Mod " + Cowlection.MODNAME + "/" + Cowlection.VERSION + " (" + Cowlection.GITURL + ")"); + + connection.getResponseCode(); + if (connection.getResponseCode() == HttpStatus.SC_NO_CONTENT) { // http status 204 + return null; + } else if (connection.getResponseCode() == HttpStatus.SC_BAD_GATEWAY && url.startsWith("https://api.hypixel.net/")) { // http status 502 (cloudflare) + throw new ApiHttpErrorException("Couldn't contact Hypixel API (502 Bad Gateway). API might be down, check https://status.hypixel.net for info.", "https://status.hypixel.net"); + } else if (connection.getResponseCode() == HttpStatus.SC_SERVICE_UNAVAILABLE) { // http status 503 Service Unavailable + int queryParamStart = url.indexOf('?', 10); + String baseUrl = queryParamStart > 0 ? url.substring(0, queryParamStart) : url; + throw new ApiHttpErrorException("Couldn't contact the API (503 Service unavailable). API might be down, or you might be blocked by Cloudflare, check if you can reach: " + baseUrl, url); + } else if (connection.getResponseCode() == HttpStatus.SC_BAD_GATEWAY && url.startsWith("https://moulberry.codes/")) { // http status 502 (cloudflare) + throw new ApiHttpErrorException("Couldn't contact Moulberry's API (502 Bad Gateway). API might be down, check if " + LOWEST_BINS + " is reachable.", LOWEST_BINS); } else { - reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + BufferedReader reader; + InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + reader = new BufferedReader(new InputStreamReader(errorStream)); + } else { + reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + } + return reader; + } + } catch (SSLHandshakeException e) { + Throwable cause = e.getCause(); + if (cause instanceof ValidatorException && cause.getCause() instanceof SunCertPathBuilderException) { + throw new ApiHttpErrorException("" + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + " ! " + + EnumChatFormatting.RED + "Java is outdated and doesn't support Let's Encrypt certificates (out of the box). A game restart might fix this issue. If the problem persists, open a ticket on the Cowshed discord server.", Cowlection.INVITE_URL); + } else { + // not a newer https related issue, thus rethrow exception: + throw e; } - return reader; } } + } diff --git a/src/main/resources/https-for-ancient-java.jks b/src/main/resources/https-for-ancient-java.jks new file mode 100644 index 0000000..2b3d337 Binary files /dev/null and b/src/main/resources/https-for-ancient-java.jks differ -- cgit