diff --git a/src/main/java/org/qora/api/ApiRequest.java b/src/main/java/org/qora/api/ApiRequest.java index 14e24356..ebd0798b 100644 --- a/src/main/java/org/qora/api/ApiRequest.java +++ b/src/main/java/org/qora/api/ApiRequest.java @@ -4,13 +4,16 @@ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; -import java.net.MalformedURLException; +import java.net.InetSocketAddress; import java.net.Socket; +import java.net.SocketAddress; import java.net.URL; import java.net.URLEncoder; import java.util.Collections; import java.util.Map; import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SNIHostName; @@ -29,23 +32,32 @@ import org.eclipse.persistence.jaxb.UnmarshallerProperties; public class ApiRequest { + private static final Pattern proxyUrlPattern = Pattern.compile("(https://)([^@:/]+)@([0-9.]{7,15})(/.*)"); + + public static class FixedIpSocket extends Socket { + private final String ipAddress; + + public FixedIpSocket(String ipAddress) { + this.ipAddress = ipAddress; + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + InetSocketAddress inetEndpoint = (InetSocketAddress) endpoint; + InetSocketAddress newEndpoint = new InetSocketAddress(ipAddress, inetEndpoint.getPort()); + super.connect(newEndpoint, timeout); + } + } + public static String perform(String uri, Map params) { if (params != null && !params.isEmpty()) uri += "?" + getParamsString(params); - InputStream in = fetchStream(uri); - if (in == null) - return null; - - try (Scanner scanner = new Scanner(in, "UTF8")) { + try (InputStream in = fetchStream(uri); Scanner scanner = new Scanner(in, "UTF8")) { scanner.useDelimiter("\\A"); return scanner.hasNext() ? scanner.next() : ""; - } finally { - try { - in.close(); - } catch (IOException e) { - // We tried... - } + } catch (IOException e) { + return null; } } @@ -55,11 +67,7 @@ public class ApiRequest { if (params != null && !params.isEmpty()) uri += "?" + getParamsString(params); - InputStream in = fetchStream(uri); - if (in == null) - return null; - - try { + try (InputStream in = fetchStream(uri)) { StreamSource json = new StreamSource(in); // Attempt to unmarshal JSON stream to Settings @@ -74,6 +82,8 @@ public class ApiRequest { throw new RuntimeException("Unable to unmarshall API response", e); } catch (JAXBException e) { throw new RuntimeException("Unable to unmarshall API response", e); + } catch (IOException e) { + throw new RuntimeException("Unable to unmarshall API response", e); } } @@ -115,39 +125,43 @@ public class ApiRequest { return resultString.length() > 0 ? resultString.substring(0, resultString.length() - 1) : resultString; } - public static InputStream fetchStream(String uri) { - try { - URL url = new URL(uri); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); + /** + * Returns InputStream for given URI. + *

+ * Also accepts special URI form:
+ * https://<hostname>@<ip-address>/... - try { - con.setRequestMethod("GET"); - con.setConnectTimeout(5000); - con.setReadTimeout(5000); - ApiRequest.setConnectionSSL(con); + * @param uri + * @return + * @throws IOException + */ + public static InputStream fetchStream(String uri) throws IOException { + String ipAddress = null; - try { - int status = con.getResponseCode(); - - if (status != 200) - return null; - } catch (IOException e) { - return null; - } - - return con.getInputStream(); - } catch (IOException e) { - return null; - } - } catch (MalformedURLException e) { - throw new RuntimeException("Malformed API request", e); - } catch (IOException e) { - // Temporary fail - return null; + // Check for special proxy form + Matcher uriMatcher = proxyUrlPattern.matcher(uri); + if (uriMatcher.matches()) { + ipAddress = uriMatcher.group(3); + uri = uriMatcher.replaceFirst(uriMatcher.group(1) + uriMatcher.group(2) + uriMatcher.group(4)); } + + URL url = new URL(uri); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + + con.setRequestMethod("GET"); + con.setConnectTimeout(5000); + con.setReadTimeout(5000); + ApiRequest.setConnectionSSL(con, ipAddress); + + int status = con.getResponseCode(); + + if (status != 200) + throw new IOException("Non-OK HTTP response"); + + return con.getInputStream(); } - public static void setConnectionSSL(HttpURLConnection con) { + public static void setConnectionSSL(HttpURLConnection con, String ipAddress) { if (!(con instanceof HttpsURLConnection)) return; @@ -155,6 +169,14 @@ public class ApiRequest { URL url = con.getURL(); httpsCon.setSSLSocketFactory(new org.bouncycastle.jsse.util.CustomSSLSocketFactory(httpsCon.getSSLSocketFactory()) { + @Override + public Socket createSocket() throws IOException { + if (ipAddress == null) + return super.createSocket(); + + return new FixedIpSocket(ipAddress); + } + @Override protected Socket configureSocket(Socket s) { if (s instanceof SSLSocket) { diff --git a/src/main/java/org/qora/controller/AutoUpdate.java b/src/main/java/org/qora/controller/AutoUpdate.java index b8c1a1c0..4fe033a6 100644 --- a/src/main/java/org/qora/controller/AutoUpdate.java +++ b/src/main/java/org/qora/controller/AutoUpdate.java @@ -148,56 +148,60 @@ public class AutoUpdate extends Thread { } private static boolean attemptUpdate(byte[] commitHash, byte[] downloadHash, String repoBaseUri) { - LOGGER.info(String.format("Fetching update from %s", repoBaseUri)); - InputStream in = ApiRequest.fetchStream(String.format(repoBaseUri, HashCode.fromBytes(commitHash).toString())); - if (in == null) { - LOGGER.warn(String.format("Failed to fetch update from %s", repoBaseUri)); - return false; // failed - try another repo - } - + String repoUri = String.format(repoBaseUri, HashCode.fromBytes(commitHash).toString()); + LOGGER.info(String.format("Fetching update from %s", repoUri)); Path newJar = Paths.get(NEW_JAR_FILENAME); - try { - MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + + try (InputStream in = ApiRequest.fetchStream(repoUri)) { + MessageDigest sha256; + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + return true; // not repo's fault + } // Save input stream into new JAR - LOGGER.debug(String.format("Saving update from %s into %s", repoBaseUri, newJar.toString())); + LOGGER.debug(String.format("Saving update from %s into %s", repoUri, newJar.toString())); - OutputStream out = Files.newOutputStream(newJar); - byte[] buffer = new byte[1024 * 1024]; - do { - int nread = in.read(buffer); - if (nread == -1) - break; + try (OutputStream out = Files.newOutputStream(newJar)) { + byte[] buffer = new byte[1024 * 1024]; + do { + int nread = in.read(buffer); + if (nread == -1) + break; - sha256.update(buffer, 0, nread); - out.write(buffer, 0, nread); - } while (true); + sha256.update(buffer, 0, nread); + out.write(buffer, 0, nread); + } while (true); + out.flush(); - // Check hash - byte[] hash = sha256.digest(); - if (!Arrays.equals(downloadHash, hash)) { - LOGGER.warn(String.format("Downloaded JAR's hash %s doesn't match %s", HashCode.fromBytes(hash).toString(), HashCode.fromBytes(downloadHash).toString())); + // Check hash + byte[] hash = sha256.digest(); + if (!Arrays.equals(downloadHash, hash)) { + LOGGER.warn(String.format("Downloaded JAR's hash %s doesn't match %s", HashCode.fromBytes(hash).toString(), HashCode.fromBytes(downloadHash).toString())); + + try { + Files.deleteIfExists(newJar); + } catch (IOException de) { + LOGGER.warn(String.format("Failed to delete download: %s", de.getMessage())); + } + + return false; + } + } catch (IOException e) { + LOGGER.warn(String.format("Failed to save update from %s into %s", repoUri, newJar.toString())); try { Files.deleteIfExists(newJar); } catch (IOException de) { - LOGGER.warn(String.format("Failed to delete download: %s", de.getMessage())); + LOGGER.warn(String.format("Failed to delete partial download: %s", de.getMessage())); } - return false; + return false; // failed - try another repo } } catch (IOException e) { - LOGGER.warn(String.format("Failed to save update from %s into %s", repoBaseUri, newJar.toString())); - - try { - Files.deleteIfExists(newJar); - } catch (IOException de) { - LOGGER.warn(String.format("Failed to delete partial download: %s", de.getMessage())); - } - + LOGGER.warn(String.format("Failed to fetch update from %s", repoUri)); return false; // failed - try another repo - } catch (NoSuchAlgorithmException e) { - return true; // not repo's fault } // Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced) diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index 4a99e979..6556b9a0 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -83,7 +83,8 @@ public class Settings { // Auto-update sources private String[] autoUpdateRepos = new String[] { - "https://github.com/catbref/qora-core/raw/%s/qora-core.jar" + "https://github.com/catbref/qora-core/raw/%s/qora-core.jar", + "https://raw.githubusercontent.com@151.101.16.133/catbref/qora-core/%s/qora-core.jar" }; // Constructors diff --git a/src/test/java/org/qora/test/ProxyTest.java b/src/test/java/org/qora/test/ProxyTest.java new file mode 100644 index 00000000..25d6a1b3 --- /dev/null +++ b/src/test/java/org/qora/test/ProxyTest.java @@ -0,0 +1,120 @@ +package org.qora.test; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.URL; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; + +public class ProxyTest { + private static final Pattern proxyUrlPattern = Pattern.compile("(https://)([^@:/]+)@([0-9.]{7,15})(/.*)"); + + public static void main(String args[]) { + String uri = "https://raw.githubusercontent.com@151.101.16.133/catbref/qora-core/894f0e54a6c22e68d4f4162b2ebcdf9b4e39162a/qora-core.jar"; + // String uri = "https://raw.githubusercontent.com/catbref/qora-core/894f0e54a6c22e68d4f4162b2ebcdf9b4e39162a/qora-core.jar"; + + try (InputStream in = fetchStream(uri)) { + int byteCount = 0; + byte[] buffer = new byte[1024 * 1024]; + do { + int nread = in.read(buffer); + if (nread == -1) + break; + + byteCount += nread; + } while (true); + + System.out.println(String.format("Fetched %d bytes", byteCount)); + } catch (IOException e) { + throw new RuntimeException("Failed: ", e); + } + } + + public static InputStream fetchStream(String uri) throws IOException { + String ipAddress = null; + + // Check for special proxy form + Matcher uriMatcher = proxyUrlPattern.matcher(uri); + if (uriMatcher.matches()) { + ipAddress = uriMatcher.group(3); + uri = uriMatcher.replaceFirst(uriMatcher.group(1) + uriMatcher.group(2) + uriMatcher.group(4)); + } + + URL url = new URL(uri); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + + con.setRequestMethod("GET"); + con.setConnectTimeout(5000); + con.setReadTimeout(5000); + setConnectionSSL(con, ipAddress); + + int status = con.getResponseCode(); + + if (status != 200) + throw new IOException("Bad response"); + + return con.getInputStream(); + } + + public static class FixedIpSocket extends Socket { + private final String ipAddress; + + public FixedIpSocket(String ipAddress) { + this.ipAddress = ipAddress; + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + InetSocketAddress inetEndpoint = (InetSocketAddress) endpoint; + InetSocketAddress newEndpoint = new InetSocketAddress(ipAddress, inetEndpoint.getPort()); + super.connect(newEndpoint, timeout); + } + } + + public static void setConnectionSSL(HttpURLConnection con, String ipAddress) { + if (!(con instanceof HttpsURLConnection)) + return; + + HttpsURLConnection httpsCon = (HttpsURLConnection) con; + URL url = con.getURL(); + + httpsCon.setSSLSocketFactory(new org.bouncycastle.jsse.util.CustomSSLSocketFactory(httpsCon.getSSLSocketFactory()) { + @Override + public Socket createSocket() throws IOException { + if (ipAddress == null) + return super.createSocket(); + + return new FixedIpSocket(ipAddress); + } + + @Override + protected Socket configureSocket(Socket s) { + if (s instanceof SSLSocket) { + SSLSocket ssl = (SSLSocket) s; + + SNIHostName sniHostName = new SNIHostName(url.getHost()); + if (null != sniHostName) { + SSLParameters sslParameters = new SSLParameters(); + + sslParameters.setServerNames(Collections.singletonList(sniHostName)); + ssl.setSSLParameters(sslParameters); + } + } + + return s; + } + }); + } + +}