Add support for fetching updates using a combination of hostname and IP address.

IP address used to create socket, hostname used for SNI, HTTPS, etc.

Added hostname+IP auto-update locations to Settings.
This commit is contained in:
catbref 2019-06-10 10:41:50 +01:00
parent 8dd4745c5c
commit 02e8bdb034
4 changed files with 228 additions and 81 deletions

View File

@ -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<String, String> 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.
* <p>
* Also accepts special URI form:<br>
* <tt>https://&lt;hostname&gt;@&lt;ip-address&gt;/...</tt>
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) {

View File

@ -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)

View File

@ -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

View File

@ -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.<SNIServerName>singletonList(sniHostName));
ssl.setSSLParameters(sslParameters);
}
}
return s;
}
});
}
}