diff --git a/app_pojavlauncher/src/main/assets/arc_dns_injector.jar b/app_pojavlauncher/src/main/assets/arc_dns_injector.jar new file mode 100644 index 000000000..3c9ddd6bf Binary files /dev/null and b/app_pojavlauncher/src/main/assets/arc_dns_injector.jar differ diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java index 7f71feffe..4366ae75c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java @@ -340,6 +340,7 @@ public class PojavLoginActivity extends BaseActivity // TODO: Remove after implement. Tools.copyAssetFile(this, "launcher_profiles.json", Tools.DIR_GAME_NEW, false); Tools.copyAssetFile(this,"resolv.conf",Tools.DIR_DATA, true); + Tools.copyAssetFile(this,"arc_dns_injector.jar",Tools.DIR_DATA, true); AssetManager am = this.getAssets(); unpackComponent(am, "caciocavallo"); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java index f005a4389..a2375ae02 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java @@ -38,6 +38,7 @@ public class LauncherPreferences public static boolean PREF_VBO_DISABLE_HACK = false; public static boolean PREF_VIRTUAL_MOUSE_START = false; public static boolean PREF_OPENGL_VERSION_HACK = false; + public static boolean PREF_ARC_CAPES = false; public static void loadPreferences(Context ctx) { @@ -71,6 +72,7 @@ public class LauncherPreferences PREF_VBO_DISABLE_HACK = DEFAULT_PREF.getBoolean("vbo_disable_hack", false); PREF_VIRTUAL_MOUSE_START = DEFAULT_PREF.getBoolean("mouse_start", false); PREF_OPENGL_VERSION_HACK = DEFAULT_PREF.getBoolean("gles_version_hack", false); + PREF_ARC_CAPES = DEFAULT_PREF.getBoolean("arc_capes",false); /* if (PREF_CUSTOM_JAVA_ARGS.isEmpty()) { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java index f62ce4dfa..d5f9e0c2c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java @@ -343,7 +343,8 @@ public class JREUtils { List userArguments = parseJavaArguments(LauncherPreferences.PREF_CUSTOM_JAVA_ARGS); String resolvFile; resolvFile = new File(Tools.DIR_DATA,"resolv.conf").getAbsolutePath(); - String[] overridableArguments = new String[]{ + + ArrayList overridableArguments = new ArrayList<>(Arrays.asList( "-Djava.home=" + Tools.DIR_HOME_JRE, "-Djava.io.tmpdir=" + ctx.getCacheDir().getAbsolutePath(), "-Duser.home=" + new File(Tools.DIR_GAME_NEW).getParent(), @@ -366,7 +367,10 @@ public class JREUtils { "-Dnet.minecraft.clientmodname=" + Tools.APP_NAME, "-Dfml.earlyprogresswindow=false" //Forge 1.14+ workaround - }; + )); + if(LauncherPreferences.PREF_ARC_CAPES) { + overridableArguments.add("-javaagent:"+new File(Tools.DIR_DATA,"arc_dns_injector.jar").getAbsolutePath()+"=23.95.137.176"); + } List additionalArguments = new ArrayList<>(); for(String arg : overridableArguments) { String strippedArg = arg.substring(0,arg.indexOf('=')); diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index 1bd9ab9f7..c56acc483 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -292,5 +292,5 @@ Force openGL 1 Help with compatibility on some old versions Arc Capes - Enables capes from Arc. For more information please visit https://arccapes.com + Enables capes from Arc. For more information please visit https://arccapes.com. Requires OptiFine. diff --git a/app_pojavlauncher/src/main/res/xml/pref_misc.xml b/app_pojavlauncher/src/main/res/xml/pref_misc.xml index dff30194a..bb241cfbf 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_misc.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_misc.xml @@ -26,6 +26,11 @@ android:key="checkLibraries" android:summary="@string/mcl_setting_check_libraries_subtitle" android:title="@string/mcl_setting_check_libraries" /> + diff --git a/arc_dns_injector/.gitignore b/arc_dns_injector/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/arc_dns_injector/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/arc_dns_injector/build.gradle b/arc_dns_injector/build.gradle new file mode 100644 index 000000000..85e789bf6 --- /dev/null +++ b/arc_dns_injector/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java-library' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 +} +jar { + manifest { + attributes("Manifest-Version": "1.0", + "PreMain-Class": "git.artdeell.arcdns.ArcDNSInjectorAgent") + } + destinationDirectory.set(file("../app_pojavlauncher/src/main/assets/")) +} \ No newline at end of file diff --git a/arc_dns_injector/src/main/java/git/artdeell/arcdns/ArcDNSInjectorAgent.java b/arc_dns_injector/src/main/java/git/artdeell/arcdns/ArcDNSInjectorAgent.java new file mode 100644 index 000000000..70fbb05fa --- /dev/null +++ b/arc_dns_injector/src/main/java/git/artdeell/arcdns/ArcDNSInjectorAgent.java @@ -0,0 +1,20 @@ +package git.artdeell.arcdns; + +public class ArcDNSInjectorAgent { + public static void premain(String args) { + System.out.println("Arc Capes DNS Injector"); + System.out.println("Parts of Alibaba's DCM library were used, please read https://github.com/alibaba/java-dns-cache-manipulator/blob/main/README.md for more info"); + try { + String[] injectedIps = new String[]{args}; + if (CacheUtilCommons.isJavaVersionAtMost8()) { + CacheUtil_J8.setInetAddressCache("s.optifine.net", injectedIps, CacheUtilCommons.NEVER_EXPIRATION); + } else { + CacheUtil_J9.setInetAddressCache("s.optifine.net", injectedIps, CacheUtilCommons.NEVER_EXPIRATION); + } + System.out.println("Added DNS cache entry: s.optifine.net/"+args); + }catch (Exception e) { + System.out.println("Failed to inject cache!"); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtilCommons.java b/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtilCommons.java new file mode 100644 index 000000000..5de79fb24 --- /dev/null +++ b/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtilCommons.java @@ -0,0 +1,324 @@ +package git.artdeell.arcdns; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import git.artdeell.arcdns.other.JavaVersion; + +public class CacheUtilCommons { + public static final long NEVER_EXPIRATION = Long.MAX_VALUE; + static InetAddress[] toInetAddressArray(String host, String[] ips) throws UnknownHostException { + InetAddress[] addresses = new InetAddress[ips.length]; + for (int i = 0; i < addresses.length; i++) { + addresses[i] = InetAddress.getByAddress(host, ip2ByteArray(ips[i])); + } + return addresses; + } + private static final String INVALID_IP_V6_ADDRESS = ": invalid IPv6 address"; + private static final String INVALID_IP_ADDRESS = ": invalid IP address"; + + static byte[] ip2ByteArray(String ip) { + boolean ipv6Expected = false; + if (ip.charAt(0) == '[') { + // This is supposed to be an IPv6 literal + if (ip.length() > 2 && ip.charAt(ip.length() - 1) == ']') { + ip = ip.substring(1, ip.length() - 1); + ipv6Expected = true; + } else { + // This was supposed to be a IPv6 address, but it's not! + throw new IllegalArgumentException(ip + INVALID_IP_V6_ADDRESS); + } + } + + if (Character.digit(ip.charAt(0), 16) != -1 || (ip.charAt(0) == ':')) { + // see if it is IPv4 address + byte[] address = textToNumericFormatV4(ip); + if (address != null) return address; + + // see if it is IPv6 address + // Check if a numeric or string zone id is present + address = textToNumericFormatV6(ip); + if (address != null) return address; + + if (ipv6Expected) { + throw new IllegalArgumentException(ip + INVALID_IP_V6_ADDRESS); + } else { + throw new IllegalArgumentException(ip + INVALID_IP_ADDRESS); + } + } else { + throw new IllegalArgumentException(ip + INVALID_IP_ADDRESS); + } + } + private static final int INADDR4SZ = 4; + private static final int INADDR16SZ = 16; + private static final int INT16SZ = 2; + + /* + * Converts IPv4 address in its textual presentation form + * into its numeric binary form. + * + * @param src a String representing an IPv4 address in standard format + * @return a byte array representing the IPv4 numeric address + */ + @SuppressWarnings("fallthrough") + static byte[] textToNumericFormatV4(String src) + { + byte[] res = new byte[INADDR4SZ]; + + long tmpValue = 0; + int currByte = 0; + boolean newOctet = true; + + int len = src.length(); + if (len == 0 || len > 15) { + return null; + } + /* + * When only one part is given, the value is stored directly in + * the network address without any byte rearrangement. + * + * When a two part address is supplied, the last part is + * interpreted as a 24-bit quantity and placed in the right + * most three bytes of the network address. This makes the + * two part address format convenient for specifying Class A + * network addresses as net.host. + * + * When a three part address is specified, the last part is + * interpreted as a 16-bit quantity and placed in the right + * most two bytes of the network address. This makes the + * three part address format convenient for specifying + * Class B net- work addresses as 128.net.host. + * + * When four parts are specified, each is interpreted as a + * byte of data and assigned, from left to right, to the + * four bytes of an IPv4 address. + * + * We determine and parse the leading parts, if any, as single + * byte values in one pass directly into the resulting byte[], + * then the remainder is treated as a 8-to-32-bit entity and + * translated into the remaining bytes in the array. + */ + for (int i = 0; i < len; i++) { + char c = src.charAt(i); + if (c == '.') { + if (newOctet || tmpValue < 0 || tmpValue > 0xff || currByte == 3) { + return null; + } + res[currByte++] = (byte) (tmpValue & 0xff); + tmpValue = 0; + newOctet = true; + } else { + int digit = Character.digit(c, 10); + if (digit < 0) { + return null; + } + tmpValue *= 10; + tmpValue += digit; + newOctet = false; + } + } + if (newOctet || tmpValue < 0 || tmpValue >= (1L << ((4 - currByte) * 8))) { + return null; + } + switch (currByte) { + case 0: + res[0] = (byte) ((tmpValue >> 24) & 0xff); + case 1: + res[1] = (byte) ((tmpValue >> 16) & 0xff); + case 2: + res[2] = (byte) ((tmpValue >> 8) & 0xff); + case 3: + res[3] = (byte) ((tmpValue >> 0) & 0xff); + } + return res; + } + + /* + * Convert IPv6 presentation level address to network order binary form. + * credit: + * Converted from C code from Solaris 8 (inet_pton) + * + * Any component of the string following a per-cent % is ignored. + * + * @param src a String representing an IPv6 address in textual format + * @return a byte array representing the IPv6 numeric address + */ + static byte[] textToNumericFormatV6(String src) + { + // Shortest valid string is "::", hence at least 2 chars + if (src.length() < 2) { + return null; + } + + int colonp; + char ch; + boolean saw_xdigit; + int val; + char[] srcb = src.toCharArray(); + byte[] dst = new byte[INADDR16SZ]; + + int srcb_length = srcb.length; + int pc = src.indexOf ('%'); + if (pc == srcb_length -1) { + return null; + } + + if (pc != -1) { + srcb_length = pc; + } + + colonp = -1; + int i = 0, j = 0; + /* Leading :: requires some special handling. */ + if (srcb[i] == ':') + if (srcb[++i] != ':') + return null; + int curtok = i; + saw_xdigit = false; + val = 0; + while (i < srcb_length) { + ch = srcb[i++]; + int chval = Character.digit(ch, 16); + if (chval != -1) { + val <<= 4; + val |= chval; + if (val > 0xffff) + return null; + saw_xdigit = true; + continue; + } + if (ch == ':') { + curtok = i; + if (!saw_xdigit) { + if (colonp != -1) + return null; + colonp = j; + continue; + } else if (i == srcb_length) { + return null; + } + if (j + INT16SZ > INADDR16SZ) + return null; + dst[j++] = (byte) ((val >> 8) & 0xff); + dst[j++] = (byte) (val & 0xff); + saw_xdigit = false; + val = 0; + continue; + } + if (ch == '.' && ((j + INADDR4SZ) <= INADDR16SZ)) { + String ia4 = src.substring(curtok, srcb_length); + /* check this IPv4 address has 3 dots, ie. A.B.C.D */ + int dot_count = 0, index=0; + while ((index = ia4.indexOf ('.', index)) != -1) { + dot_count ++; + index ++; + } + if (dot_count != 3) { + return null; + } + byte[] v4addr = textToNumericFormatV4(ia4); + if (v4addr == null) { + return null; + } + for (int k = 0; k < INADDR4SZ; k++) { + dst[j++] = v4addr[k]; + } + saw_xdigit = false; + break; /* '\0' was seen by inet_pton4(). */ + } + return null; + } + if (saw_xdigit) { + if (j + INT16SZ > INADDR16SZ) + return null; + dst[j++] = (byte) ((val >> 8) & 0xff); + dst[j++] = (byte) (val & 0xff); + } + + if (colonp != -1) { + int n = j - colonp; + + if (j == INADDR16SZ) + return null; + for (i = 1; i <= n; i++) { + dst[INADDR16SZ - i] = dst[colonp + n - i]; + dst[colonp + n - i] = 0; + } + j = INADDR16SZ; + } + if (j != INADDR16SZ) + return null; + byte[] newdst = convertFromIPv4MappedAddress(dst); + if (newdst != null) { + return newdst; + } else { + return dst; + } + } + + /* + * Convert IPv4-Mapped address to IPv4 address. Both input and + * returned value are in network order binary form. + * + * @param src a String representing an IPv4-Mapped address in textual format + * @return a byte array representing the IPv4 numeric address + */ + private static byte[] convertFromIPv4MappedAddress(byte[] addr) { + if (isIPv4MappedAddress(addr)) { + byte[] newAddr = new byte[INADDR4SZ]; + System.arraycopy(addr, 12, newAddr, 0, INADDR4SZ); + return newAddr; + } + return null; + } + + /** + * Utility routine to check if the InetAddress is an + * IPv4 mapped IPv6 address. + * + * @return a boolean indicating if the InetAddress is + * an IPv4 mapped IPv6 address; or false if address is IPv4 address. + */ + private static boolean isIPv4MappedAddress(byte[] addr) { + if (addr.length < INADDR16SZ) { + return false; + } + if ((addr[0] == 0x00) && (addr[1] == 0x00) && + (addr[2] == 0x00) && (addr[3] == 0x00) && + (addr[4] == 0x00) && (addr[5] == 0x00) && + (addr[6] == 0x00) && (addr[7] == 0x00) && + (addr[8] == 0x00) && (addr[9] == 0x00) && + (addr[10] == (byte)0xff) && + (addr[11] == (byte)0xff)) { + return true; + } + return false; + } + public static boolean isJavaVersionAtMost8() { + return JAVA_SPECIFICATION_VERSION_AS_ENUM.atMost(JavaVersion.JAVA_1_8); + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Below source code is copied from commons-lang-3.12.0: + // + // https://github.com/apache/commons-lang/blob/rel/commons-lang-3.12.0/src/main/java/org/apache/commons/lang3/SystemUtils.java + // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + private static final String JAVA_SPECIFICATION_VERSION = getSystemProperty("java.specification.version"); + private static final JavaVersion JAVA_SPECIFICATION_VERSION_AS_ENUM = JavaVersion.get(JAVA_SPECIFICATION_VERSION); + + @SuppressWarnings({"CommentedOutCode", "SameParameterValue"}) + private static String getSystemProperty(final String property) { + try { + return System.getProperty(property); + } catch (final SecurityException ex) { + // we are not allowed to look at this property + // System.err.println("Caught a SecurityException reading the system property '" + property + // + "'; the SystemUtils property value will default to null."); + return null; + } + } + +} diff --git a/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtil_J8.java b/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtil_J8.java new file mode 100644 index 000000000..7c53da9be --- /dev/null +++ b/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtil_J8.java @@ -0,0 +1,153 @@ +package git.artdeell.arcdns; + +import static git.artdeell.arcdns.CacheUtilCommons.NEVER_EXPIRATION; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; + + +public final class CacheUtil_J8 { + + public static void setInetAddressCache(String host, String[] ips, long expireMillis) + throws UnknownHostException, IllegalAccessException, InstantiationException, + InvocationTargetException, ClassNotFoundException, NoSuchFieldException { + host = host.toLowerCase(); + long expiration = expireMillis == NEVER_EXPIRATION ? NEVER_EXPIRATION : System.currentTimeMillis() + expireMillis; + Object entry = newCacheEntry(host, ips, expiration); + + synchronized (getAddressCacheOfInetAddress()) { + getCache().put(host, entry); + getNegativeCache().remove(host); + } + } + + private static Object newCacheEntry(String host, String[] ips, long expiration) + throws UnknownHostException, ClassNotFoundException, IllegalAccessException, + InvocationTargetException, InstantiationException { + // InetAddress.CacheEntry has only one constructor + return getConstructorOfInetAddress$CacheEntry().newInstance(CacheUtilCommons.toInetAddressArray(host, ips), expiration); + } + + private static volatile Constructor constructorOfInetAddress$CacheEntry = null; + + private static Constructor getConstructorOfInetAddress$CacheEntry() throws ClassNotFoundException { + if (constructorOfInetAddress$CacheEntry != null) return constructorOfInetAddress$CacheEntry; + + synchronized (CacheUtilCommons.class) { + // double check + if (constructorOfInetAddress$CacheEntry != null) return constructorOfInetAddress$CacheEntry; + + final String className = "java.net.InetAddress$CacheEntry"; + final Class clazz = Class.forName(className); + + // InetAddress.CacheEntry has only one constructor: + // - for jdk 6, constructor signature is CacheEntry(Object address, long expiration) + // - for jdk 7/8, constructor signature is CacheEntry(InetAddress[] addresses, long expiration) + // + // code in jdk 6: + // https://hg.openjdk.java.net/jdk6/jdk6/jdk/file/8deef18bb749/src/share/classes/java/net/InetAddress.java#l739 + // code in jdk 7: + // https://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/4dd5e486620d/src/share/classes/java/net/InetAddress.java#l742 + // code in jdk 8: + // https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/45e4e636b757/src/share/classes/java/net/InetAddress.java#l748 + final Constructor constructor = clazz.getDeclaredConstructors()[0]; + constructor.setAccessible(true); + + constructorOfInetAddress$CacheEntry = constructor; + return constructor; + } + } + + public static void removeInetAddressCache(String host) + throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + host = host.toLowerCase(); + + synchronized (getAddressCacheOfInetAddress()) { + getCache().remove(host); + getNegativeCache().remove(host); + } + } + + private static Map getCache() + throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + return getCacheOfInetAddress$Cache0(getAddressCacheOfInetAddress()); + } + + private static Map getNegativeCache() + throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + return getCacheOfInetAddress$Cache0(getNegativeCacheOfInetAddress()); + } + + + private static volatile Field cacheMapFieldOfInetAddress$Cache = null; + + @SuppressWarnings("unchecked") + private static Map getCacheOfInetAddress$Cache0(Object inetAddressCache) + throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + if (cacheMapFieldOfInetAddress$Cache == null) { + synchronized (CacheUtil_J8.class) { + if (cacheMapFieldOfInetAddress$Cache == null) { // double check + final Class clazz = Class.forName("java.net.InetAddress$Cache"); + final Field f = clazz.getDeclaredField("cache"); + f.setAccessible(true); + cacheMapFieldOfInetAddress$Cache = f; + } + } + } + + return (Map) cacheMapFieldOfInetAddress$Cache.get(inetAddressCache); + } + + private static Object getAddressCacheOfInetAddress() + throws NoSuchFieldException, IllegalAccessException { + return getAddressCacheAndNegativeCacheOfInetAddress0()[0]; + } + + private static Object getNegativeCacheOfInetAddress() + throws NoSuchFieldException, IllegalAccessException { + return getAddressCacheAndNegativeCacheOfInetAddress0()[1]; + } + + private static volatile Object[] ADDRESS_CACHE_AND_NEGATIVE_CACHE = null; + + private static Object[] getAddressCacheAndNegativeCacheOfInetAddress0() + throws NoSuchFieldException, IllegalAccessException { + if (ADDRESS_CACHE_AND_NEGATIVE_CACHE != null) return ADDRESS_CACHE_AND_NEGATIVE_CACHE; + + synchronized (CacheUtil_J8.class) { + // double check + if (ADDRESS_CACHE_AND_NEGATIVE_CACHE != null) return ADDRESS_CACHE_AND_NEGATIVE_CACHE; + + final Field cacheField = InetAddress.class.getDeclaredField("addressCache"); + cacheField.setAccessible(true); + + final Field negativeCacheField = InetAddress.class.getDeclaredField("negativeCache"); + negativeCacheField.setAccessible(true); + + ADDRESS_CACHE_AND_NEGATIVE_CACHE = new Object[]{ + cacheField.get(InetAddress.class), + negativeCacheField.get(InetAddress.class) + }; + return ADDRESS_CACHE_AND_NEGATIVE_CACHE; + } + } + + private static boolean isDnsCacheEntryExpired(String host) { + return null == host || "0.0.0.0".equals(host); + } + + public static void clearInetAddressCache() + throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + synchronized (getAddressCacheOfInetAddress()) { + getCache().clear(); + getNegativeCache().clear(); + } + } + + private CacheUtil_J8() { + } +} diff --git a/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtil_J9.java b/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtil_J9.java new file mode 100644 index 000000000..b54060bae --- /dev/null +++ b/arc_dns_injector/src/main/java/git/artdeell/arcdns/CacheUtil_J9.java @@ -0,0 +1,145 @@ +package git.artdeell.arcdns; +import static git.artdeell.arcdns.CacheUtilCommons.NEVER_EXPIRATION; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListSet; +public class CacheUtil_J9 { + public static void setInetAddressCache(String host, String[] ips, long expireMillis) + throws UnknownHostException, IllegalAccessException, InstantiationException, + InvocationTargetException, ClassNotFoundException, NoSuchFieldException { + long expiration = expireMillis == NEVER_EXPIRATION ? NEVER_EXPIRATION : System.nanoTime() + expireMillis * 1_000_000; + Object cachedAddresses = newCachedAddresses(host, ips, expiration); + + getCacheOfInetAddress().put(host, cachedAddresses); + getExpirySetOfInetAddress().add(cachedAddresses); + } + + private static Object newCachedAddresses(String host, String[] ips, long expiration) + throws ClassNotFoundException, UnknownHostException, IllegalAccessException, + InvocationTargetException, InstantiationException { + // InetAddress.CachedAddresses has only one constructor + return getConstructorOfInetAddress$CachedAddresses().newInstance(host, CacheUtilCommons.toInetAddressArray(host, ips), expiration); + } + + private static volatile Constructor constructorOfInetAddress$CachedAddresses = null; + + private static Constructor getConstructorOfInetAddress$CachedAddresses() throws ClassNotFoundException { + if (constructorOfInetAddress$CachedAddresses != null) return constructorOfInetAddress$CachedAddresses; + + synchronized (CacheUtilCommons.class) { + // double check + if (constructorOfInetAddress$CachedAddresses != null) return constructorOfInetAddress$CachedAddresses; + + final Class clazz = Class.forName(inetAddress$CachedAddresses_ClassName); + + // InetAddress.CacheEntry has only one constructor: + // + // - for jdk 9-jdk12, constructor signature is CachedAddresses(String host, InetAddress[] inetAddresses, long expiryTime) + // code in jdk 9: + // https://hg.openjdk.java.net/jdk9/jdk9/jdk/file/65464a307408/src/java.base/share/classes/java/net/InetAddress.java#l783 + // code in jdk 11: + // https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/java.base/share/classes/java/net/InetAddress.java#l787 + final Constructor constructor = clazz.getDeclaredConstructors()[0]; + constructor.setAccessible(true); + + constructorOfInetAddress$CachedAddresses = constructor; + return constructor; + } + } + + public static void removeInetAddressCache(String host) throws NoSuchFieldException, IllegalAccessException { + getCacheOfInetAddress().remove(host); + removeHostFromExpirySetOfInetAddress(host); + } + + /** + * @see #getExpirySetOfInetAddress() + */ + private static void removeHostFromExpirySetOfInetAddress(String host) + throws NoSuchFieldException, IllegalAccessException { + for (Iterator iterator = getExpirySetOfInetAddress().iterator(); iterator.hasNext(); ) { + Object cachedAddresses = iterator.next(); + if (getHostOfInetAddress$CacheAddress(cachedAddresses).equals(host)) { + iterator.remove(); + } + } + } + + private static volatile Field hostFieldOfInetAddress$CacheAddress = null; + + private static String getHostOfInetAddress$CacheAddress(Object cachedAddresses) + throws NoSuchFieldException, IllegalAccessException { + if (hostFieldOfInetAddress$CacheAddress == null) { + synchronized (CacheUtil_J9.class) { + if (hostFieldOfInetAddress$CacheAddress == null) { // double check + final Field f = cachedAddresses.getClass().getDeclaredField("host"); + f.setAccessible(true); + hostFieldOfInetAddress$CacheAddress = f; + } + } + } + return (String) hostFieldOfInetAddress$CacheAddress.get(cachedAddresses); + } + + + ////////////////////////////////////////////////////////////////////////////// + // getters of static cache related fields of InetAddress + ////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("unchecked") + private static ConcurrentMap getCacheOfInetAddress() + throws NoSuchFieldException, IllegalAccessException { + return (ConcurrentMap) getCacheAndExpirySetOfInetAddress0()[0]; + } + + @SuppressWarnings("unchecked") + private static ConcurrentSkipListSet getExpirySetOfInetAddress() + throws NoSuchFieldException, IllegalAccessException { + return (ConcurrentSkipListSet) getCacheAndExpirySetOfInetAddress0()[1]; + } + + private static volatile Object[] ADDRESS_CACHE_AND_EXPIRY_SET = null; + + private static Object[] getCacheAndExpirySetOfInetAddress0() + throws NoSuchFieldException, IllegalAccessException { + if (ADDRESS_CACHE_AND_EXPIRY_SET != null) return ADDRESS_CACHE_AND_EXPIRY_SET; + + synchronized (CacheUtil_J9.class) { + if (ADDRESS_CACHE_AND_EXPIRY_SET != null) return ADDRESS_CACHE_AND_EXPIRY_SET; + + final Field cacheField = InetAddress.class.getDeclaredField("cache"); + cacheField.setAccessible(true); + + final Field expirySetField = InetAddress.class.getDeclaredField("expirySet"); + expirySetField.setAccessible(true); + + ADDRESS_CACHE_AND_EXPIRY_SET = new Object[]{ + cacheField.get(InetAddress.class), + expirySetField.get(InetAddress.class) + }; + + return ADDRESS_CACHE_AND_EXPIRY_SET; + } + } + + ////////////////////////////////////////////////////////////////////////////// + + private static final String inetAddress$CachedAddresses_ClassName = "java.net.InetAddress$CachedAddresses"; + public static void clearInetAddressCache() throws NoSuchFieldException, IllegalAccessException { + getCacheOfInetAddress().clear(); + getExpirySetOfInetAddress().clear(); + } + + private CacheUtil_J9() { + } + +} diff --git a/arc_dns_injector/src/main/java/git/artdeell/arcdns/other/JavaVersion.java b/arc_dns_injector/src/main/java/git/artdeell/arcdns/other/JavaVersion.java new file mode 100644 index 000000000..2f0afb47d --- /dev/null +++ b/arc_dns_injector/src/main/java/git/artdeell/arcdns/other/JavaVersion.java @@ -0,0 +1,366 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git.artdeell.arcdns.other; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// This source code file is copied and small adopted from commons-lang-3.12.0: +// +// https://github.com/apache/commons-lang/blob/rel/commons-lang-3.12.0/src/main/java/org/apache/commons/lang3/JavaVersion.java +// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +/** + *

An enum representing all the versions of the Java specification. + * This is intended to mirror available values from the + * java.specification.version System property.

+ * + * @since 3.0 + */ +@SuppressWarnings({"unused", "SameParameterValue"}) +public +enum JavaVersion { + + /** + * The Java version reported by Android. This is not an official Java version number. + */ + JAVA_0_9(1.5f, "0.9"), + + /** + * Java 1.1. + */ + JAVA_1_1(1.1f, "1.1"), + + /** + * Java 1.2. + */ + JAVA_1_2(1.2f, "1.2"), + + /** + * Java 1.3. + */ + JAVA_1_3(1.3f, "1.3"), + + /** + * Java 1.4. + */ + JAVA_1_4(1.4f, "1.4"), + + /** + * Java 1.5. + */ + JAVA_1_5(1.5f, "1.5"), + + /** + * Java 1.6. + */ + JAVA_1_6(1.6f, "1.6"), + + /** + * Java 1.7. + */ + JAVA_1_7(1.7f, "1.7"), + + /** + * Java 1.8. + */ + JAVA_1_8(1.8f, "1.8"), + + /** + * Java 1.9. + * + * @deprecated As of release 3.5, replaced by {@link #JAVA_9} + */ + @Deprecated + JAVA_1_9(9.0f, "9"), + + /** + * Java 9. + * + * @since 3.5 + */ + JAVA_9(9.0f, "9"), + + /** + * Java 10. + * + * @since 3.7 + */ + JAVA_10(10.0f, "10"), + + /** + * Java 11. + * + * @since 3.8 + */ + JAVA_11(11.0f, "11"), + + /** + * Java 12. + * + * @since 3.9 + */ + JAVA_12(12.0f, "12"), + + /** + * Java 13. + * + * @since 3.9 + */ + JAVA_13(13.0f, "13"), + + /** + * Java 14. + * + * @since 3.11 + */ + JAVA_14(14.0f, "14"), + + /** + * Java 15. + * + * @since 3.11 + */ + JAVA_15(15.0f, "15"), + + /** + * Java 16. + * + * @since 3.11 + */ + JAVA_16(16.0f, "16"), + + /** + * Java 17. + * + * @since 3.12.0 + */ + JAVA_17(17.0f, "17"), + + /** + * The most recent java version. Mainly introduced to avoid to break when a new version of Java is used. + */ + JAVA_RECENT(maxVersion(), Float.toString(maxVersion())); + + /** + * The float value. + */ + private final float value; + + /** + * The standard name. + */ + private final String name; + + /** + * Constructor. + * + * @param value the float value + * @param name the standard name, not null + */ + JavaVersion(final float value, final String name) { + this.value = value; + this.name = name; + } + + //----------------------------------------------------------------------- + + /** + *

Whether this version of Java is at least the version of Java passed in.

+ * + *

For example:
+ * {@code myVersion.atLeast(JavaVersion.JAVA_1_4)}

+ * + * @param requiredVersion the version to check against, not null + * @return true if this version is equal to or greater than the specified version + */ + public boolean atLeast(final JavaVersion requiredVersion) { + return this.value >= requiredVersion.value; + } + + //----------------------------------------------------------------------- + + /** + *

Whether this version of Java is at most the version of Java passed in.

+ * + *

For example:
+ * {@code myVersion.atMost(JavaVersion.JAVA_1_4)}

+ * + * @param requiredVersion the version to check against, not null + * @return true if this version is equal to or greater than the specified version + * @since 3.9 + */ + public boolean atMost(final JavaVersion requiredVersion) { + return this.value <= requiredVersion.value; + } + + /** + * Transforms the given string with a Java version number to the + * corresponding constant of this enumeration class. This method is used + * internally. + * + * @param nom the Java version as string + * @return the corresponding enumeration constant or null if the + * version is unknown + */ + // helper for static importing + static JavaVersion getJavaVersion(final String nom) { + return get(nom); + } + + /** + * Transforms the given string with a Java version number to the + * corresponding constant of this enumeration class. This method is used + * internally. + * + * @param versionStr the Java version as string + * @return the corresponding enumeration constant or null if the + * version is unknown + */ + public static JavaVersion get(final String versionStr) { + if (versionStr == null) { + return null; + } + switch (versionStr) { + case "0.9": + return JAVA_0_9; + case "1.1": + return JAVA_1_1; + case "1.2": + return JAVA_1_2; + case "1.3": + return JAVA_1_3; + case "1.4": + return JAVA_1_4; + case "1.5": + return JAVA_1_5; + case "1.6": + return JAVA_1_6; + case "1.7": + return JAVA_1_7; + case "1.8": + return JAVA_1_8; + case "9": + return JAVA_9; + case "10": + return JAVA_10; + case "11": + return JAVA_11; + case "12": + return JAVA_12; + case "13": + return JAVA_13; + case "14": + return JAVA_14; + case "15": + return JAVA_15; + case "16": + return JAVA_16; + case "17": + return JAVA_17; + } + final float v = toFloatVersion(versionStr); + if ((v - 1.) < 1.) { // then we need to check decimals > .9 + final int firstComma = Math.max(versionStr.indexOf('.'), versionStr.indexOf(',')); + final int end = Math.max(versionStr.length(), versionStr.indexOf(',', firstComma)); + if (Float.parseFloat(versionStr.substring(firstComma + 1, end)) > .9f) { + return JAVA_RECENT; + } + } else if (v > 10) { + return JAVA_RECENT; + } + return null; + } + + //----------------------------------------------------------------------- + + /** + *

The string value is overridden to return the standard name.

+ * + *

For example, {@code "1.5"}.

+ * + * @return the name, not null + */ + @Override + public String toString() { + return name; + } + + /** + * Gets the Java Version from the system or 99.0 if the {@code java.specification.version} system property is not set. + * + * @return the value of {@code java.specification.version} system property or 99.0 if it is not set. + */ + private static float maxVersion() { + final float v = toFloatVersion(System.getProperty("java.specification.version", "99.0")); + if (v > 0) { + return v; + } + return 99f; + } + + /** + * Parses a float value from a String. + * + * @param value the String to parse. + * @return the float value represented by the string or -1 if the given String can not be parsed. + */ + private static float toFloatVersion(final String value) { + final int defaultReturnValue = -1; + if (value.contains(".")) { + final String[] toParse = value.split("\\."); + if (toParse.length >= 2) { + return toFloat(toParse[0] + '.' + toParse[1], defaultReturnValue); + } + } else { + return toFloat(value, defaultReturnValue); + } + return defaultReturnValue; + } + + /** + *

Convert a {@code String} to a {@code float}, returning a + * default value if the conversion fails.

+ * + *

If the string {@code str} is {@code null}, the default + * value is returned.

+ * + *
+     *   NumberUtils.toFloat(null, 1.1f)   = 1.0f
+     *   NumberUtils.toFloat("", 1.1f)     = 1.1f
+     *   NumberUtils.toFloat("1.5", 0.0f)  = 1.5f
+     * 
+ * + * @param str the string to convert, may be {@code null} + * @param defaultValue the default value + * @return the float represented by the string, or defaultValue + * if conversion fails + * @since 2.1 + */ + private static float toFloat(final String str, final float defaultValue) { + if (str == null) { + return defaultValue; + } + try { + return Float.parseFloat(str); + } catch (final NumberFormatException nfe) { + return defaultValue; + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 1e810efdd..cb6045552 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,4 @@ rootProject.name='PojavLauncher' include ':jre_lwjgl3glfw' include ':app_pojavlauncher' +include ':arc_dns_injector'