diff --git a/src/org/lineageos/updater/controller/UpdaterController.java b/src/org/lineageos/updater/controller/UpdaterController.java index d6ed4016..3987a90d 100644 --- a/src/org/lineageos/updater/controller/UpdaterController.java +++ b/src/org/lineageos/updater/controller/UpdaterController.java @@ -375,6 +375,7 @@ public class UpdaterController implements Controller { .setDestination(update.getFile()) .setDownloadCallback(getDownloadCallback(downloadId)) .setProgressListener(getProgressListener(downloadId)) + .setUseDuplicateLinks(true) .build(); } catch (IOException exception) { Log.e(TAG, "Could not build download client"); @@ -417,6 +418,7 @@ public class UpdaterController implements Controller { .setDestination(update.getFile()) .setDownloadCallback(getDownloadCallback(downloadId)) .setProgressListener(getProgressListener(downloadId)) + .setUseDuplicateLinks(true) .build(); } catch (IOException exception) { Log.e(TAG, "Could not build download client"); diff --git a/src/org/lineageos/updater/download/DownloadClient.java b/src/org/lineageos/updater/download/DownloadClient.java index 8ccf3e8a..6a2a4906 100644 --- a/src/org/lineageos/updater/download/DownloadClient.java +++ b/src/org/lineageos/updater/download/DownloadClient.java @@ -63,6 +63,7 @@ public interface DownloadClient { private File mDestination; private DownloadClient.DownloadCallback mCallback; private DownloadClient.ProgressListener mProgressListener; + private boolean mUseDuplicateLinks; public DownloadClient build() throws IOException { if (mUrl == null) { @@ -72,7 +73,8 @@ public interface DownloadClient { } else if (mCallback == null) { throw new IllegalStateException("No download callback defined"); } - return new HttpURLConnectionClient(mUrl, mDestination, mProgressListener, mCallback); + return new HttpURLConnectionClient(mUrl, mDestination, mProgressListener, mCallback, + mUseDuplicateLinks); } public Builder setUrl(String url) { @@ -94,5 +96,10 @@ public interface DownloadClient { mProgressListener = progressListener; return this; } + + public Builder setUseDuplicateLinks(boolean useDuplicateLinks) { + mUseDuplicateLinks = useDuplicateLinks; + return this; + } } } diff --git a/src/org/lineageos/updater/download/HttpURLConnectionClient.java b/src/org/lineageos/updater/download/HttpURLConnectionClient.java index 89d47afe..afee518b 100644 --- a/src/org/lineageos/updater/download/HttpURLConnectionClient.java +++ b/src/org/lineageos/updater/download/HttpURLConnectionClient.java @@ -25,18 +25,23 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.PriorityQueue; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class HttpURLConnectionClient implements DownloadClient { private final static String TAG = "HttpURLConnectionClient"; - private final HttpURLConnection mClient; + private HttpURLConnection mClient; private final File mDestination; private final DownloadClient.ProgressListener mProgressListener; private final DownloadClient.DownloadCallback mCallback; + private final boolean mUseDuplicateLinks; private DownloadThread mDownloadThread; @@ -54,11 +59,13 @@ public class HttpURLConnectionClient implements DownloadClient { HttpURLConnectionClient(String url, File destination, DownloadClient.ProgressListener progressListener, - DownloadClient.DownloadCallback callback) throws IOException { + DownloadClient.DownloadCallback callback, + boolean useDuplicateLinks) throws IOException { mClient = (HttpURLConnection) new URL(url).openConnection(); mDestination = destination; mProgressListener = progressListener; mCallback = callback; + mUseDuplicateLinks = useDuplicateLinks; } @Override @@ -113,6 +120,10 @@ public class HttpURLConnectionClient implements DownloadClient { return (statusCode / 100) == 2; } + private static boolean isRedirectCode(int statusCode) { + return (statusCode / 100) == 3; + } + private static boolean isPartialContentCode(int statusCode) { return statusCode == 206; } @@ -155,11 +166,100 @@ public class HttpURLConnectionClient implements DownloadClient { } } + private void changeClientUrl(URL newUrl) throws IOException { + String range = mClient.getRequestProperty("Range"); + mClient.disconnect(); + mClient = (HttpURLConnection) newUrl.openConnection(); + if (range != null) { + mClient.setRequestProperty("Range", range); + } + } + + private void handleDuplicateLinks() throws IOException { + String protocol = mClient.getURL().getProtocol(); + + class DuplicateLink { + private String mUrl; + private int mPriority; + private DuplicateLink(String url, int priority) { + mUrl = url; + mPriority = priority; + } + } + + PriorityQueue duplicates = null; + + for (Map.Entry> entry : mClient.getHeaderFields().entrySet()) { + if ("Link".equalsIgnoreCase((entry.getKey()))) { + duplicates = new PriorityQueue<>(entry.getValue().size(), + new Comparator() { + @Override + public int compare(DuplicateLink d1, DuplicateLink d2) { + return Integer.compare(d1.mPriority, d2.mPriority); + } + }); + + // https://tools.ietf.org/html/rfc6249 + // https://tools.ietf.org/html/rfc5988#section-5 + String regex = "(?i)<(.+)>\\s*;\\s*rel=duplicate(?:.*pri=([0-9]+).*|.*)?"; + Pattern pattern = Pattern.compile(regex); + for (String field : entry.getValue()) { + Matcher matcher = pattern.matcher(field); + if (matcher.matches()) { + String url = matcher.group(1); + String pri = matcher.group(2); + int priority = pri != null ? Integer.parseInt(pri) : 999999; + duplicates.add(new DuplicateLink(url, priority)); + Log.d(TAG, "Adding duplicate link " + url); + } else { + Log.d(TAG, "Ignoring link " + field); + } + } + } + } + + String newUrl = mClient.getHeaderField("Location"); + for (;;) { + try { + URL url = new URL(newUrl); + if (!url.getProtocol().equals(protocol)) { + // If we hadn't handled duplicate links, we wouldn't have + // used this url. + throw new IOException("Protocol changes are not allowed"); + } + Log.d(TAG, "Downloading from " + newUrl); + changeClientUrl(url); + mClient.setConnectTimeout(5000); + mClient.connect(); + if (!isSuccessCode(mClient.getResponseCode())) { + throw new IOException("Server replied with " + mClient.getResponseCode()); + } + return; + } catch (IOException e) { + if (duplicates != null && !duplicates.isEmpty()) { + DuplicateLink link = duplicates.poll(); + duplicates.remove(link); + newUrl = link.mUrl; + Log.e(TAG, "Using duplicate link " + link.mUrl, e); + } else { + throw e; + } + } + } + } + @Override public void run() { try { + mClient.setInstanceFollowRedirects(!mUseDuplicateLinks); mClient.connect(); int responseCode = mClient.getResponseCode(); + + if (mUseDuplicateLinks && isRedirectCode(responseCode)) { + handleDuplicateLinks(); + responseCode = mClient.getResponseCode(); + } + mCallback.onResponse(responseCode, mClient.getURL().toString(), new Headers()); if (mResume && isPartialContentCode(responseCode)) {